diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-05 00:14:01 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-05 00:14:01 +0300 |
commit | f4056ff4474daf3da66ceaf4473306b0c4652897 (patch) | |
tree | ba6ca64ede0a7ec8d2c6971c7f3f5b3d8ab5f81d /app | |
parent | 795b6eb292706d577c13556a3583897f082dda6e (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
12 files changed, 245 insertions, 262 deletions
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 20f82500a02..e45fd508a5b 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -211,19 +211,6 @@ export default { return this.getNoteableData.current_user.can_create_note; }, }, - watch: { - 'idState.moreActionsShown': { - handler(val) { - const el = this.$el.closest('.vue-recycle-scroller__item-view'); - - if (el) { - // We can't add a style with Vue because of the way the virtual - // scroller library renders the diff files - el.style.zIndex = val ? '1' : null; - } - }, - }, - }, methods: { ...mapActions('diffs', [ 'toggleFileDiscussions', diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 928f81daf92..b29755545f2 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -67,8 +67,6 @@ export default { originalInput: '', users: [], selectedTokens: [], - hasBeenFocused: false, - hideDropdownWithNoItems: true, }; }, computed: { @@ -124,7 +122,6 @@ export default { }, methods: { handleTextInput(inputQuery) { - this.hideDropdownWithNoItems = false; this.originalInput = inputQuery; this.query = inputQuery.trim(); this.loading = true; @@ -161,18 +158,10 @@ export default { handleInput() { this.$emit('input', this.selectedTokens); }, - handleBlur() { - this.hideDropdownWithNoItems = false; - }, handleFocus() { - // The modal auto-focuses on the input when opened. - // This prevents the dropdown from opening when the modal opens. - if (this.hasBeenFocused) { - this.loading = true; - this.retrieveUsers(); - } - - this.hasBeenFocused = true; + // Search for users when focused on the input + this.loading = true; + this.retrieveUsers(); }, handleTokenRemove(value) { if (this.selectedTokens.length) { @@ -208,11 +197,9 @@ export default { :dropdown-items="users" :loading="loading" :allow-user-defined-tokens="emailIsValid" - :hide-dropdown-with-no-items="hideDropdownWithNoItems" :placeholder="placeholderText" :aria-labelledby="ariaLabelledby" :text-input-attrs="textInputAttrs" - @blur="handleBlur" @text-input="handleTextInput" @input="handleInput" @focus="handleFocus" diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 264dbff525b..4ec57676b79 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -204,7 +204,8 @@ export default { return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE; }, isBlameEnabled() { - return this.glFeatures.blobBlameInfo && this.blobInfo.language === 'json'; // This feature is currently scoped to JSON files + // Blame information within the blob viewer is not yet supported in our fallback (HAML) viewers + return this.glFeatures.blobBlameInfo && !this.useFallback; }, }, watch: { @@ -295,7 +296,14 @@ export default { }, handleToggleBlame() { this.switchViewer(SIMPLE_BLOB_VIEWER); - this.showBlame = !this.showBlame; + + if (this.$route?.query?.plain === '0') { + // If the user is not viewing plain code and clicks the blame button, we always want to show blame info + // For instance, when viewing the rendered version of a Markdown file + this.showBlame = true; + } else { + this.showBlame = !this.showBlame; + } const blame = this.showBlame === true ? '1' : '0'; if (this.$route?.query?.blame === blame) return; diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js index b248681f053..7627b2e0e08 100644 --- a/app/assets/javascripts/search/store/mutations.js +++ b/app/assets/javascripts/search/store/mutations.js @@ -6,7 +6,7 @@ export default { }, [types.RECEIVE_GROUPS_SUCCESS](state, data) { state.fetchingGroups = false; - state.groups = data; + state.groups = [...data]; }, [types.RECEIVE_GROUPS_ERROR](state) { state.fetchingGroups = false; @@ -17,7 +17,7 @@ export default { }, [types.RECEIVE_PROJECTS_SUCCESS](state, data) { state.fetchingProjects = false; - state.projects = data; + state.projects = [...data]; }, [types.RECEIVE_PROJECTS_ERROR](state) { state.fetchingProjects = false; diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue index d9f824b6e18..5bee757856f 100644 --- a/app/assets/javascripts/search/topbar/components/app.vue +++ b/app/assets/javascripts/search/topbar/components/app.vue @@ -16,15 +16,9 @@ export default { i18n: { searchPlaceholder: s__(`GlobalSearch|Search for projects, issues, etc.`), searchLabel: s__(`GlobalSearch|What are you searching for?`), - documentFetchErrorMessage: s__( - 'GlobalSearch|There was an error fetching the "Syntax Options" document.', - ), - searchFieldLabel: s__('GlobalSearch|What are you searching for?'), syntaxOptionsLabel: s__('GlobalSearch|Syntax options'), groupFieldLabel: s__('GlobalSearch|Group'), projectFieldLabel: s__('GlobalSearch|Project'), - searchButtonLabel: s__('GlobalSearch|Search'), - closeButtonLabel: s__('GlobalSearch|Close'), }, components: { GlButton, @@ -124,17 +118,20 @@ export default { @submit="applyQuery" /> </div> - <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-3"> - <label class="gl-display-block gl-mb-1 gl-md-pb-2">{{ + <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-3 gl-min-w-20"> + <label id="groupfilterDropdown" class="gl-display-block gl-mb-1 gl-md-pb-2">{{ $options.i18n.groupFieldLabel }}</label> - <group-filter :initial-data="groupInitialJson" /> + <group-filter label-id="groupfilterDropdown" :group-initial-json="groupInitialJson" /> </div> - <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-ml-3"> - <label class="gl-display-block gl-mb-1 gl-md-pb-2">{{ + <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-ml-3 gl-min-w-20"> + <label id="projectfilterDropdown" class="gl-display-block gl-mb-1 gl-md-pb-2">{{ $options.i18n.projectFieldLabel }}</label> - <project-filter :initial-data="projectInitialJson" /> + <project-filter + label-id="projectfilterDropdown" + :project-initial-json="projectInitialJson" + /> </div> </div> </div> diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/topbar/components/group_filter.vue index a177eb28991..7f13def8a0f 100644 --- a/app/assets/javascripts/search/topbar/components/group_filter.vue +++ b/app/assets/javascripts/search/topbar/components/group_filter.vue @@ -12,27 +12,46 @@ export default { SearchableDropdown, }, props: { - initialData: { + groupInitialJson: { type: Object, required: false, default: () => ({}), }, + labelId: { + type: String, + required: false, + default: 'labelId', + }, + }, + data() { + return { + search: '', + }; }, computed: { ...mapState(['query', 'groups', 'fetchingGroups']), ...mapGetters(['frequentGroups', 'currentScope']), selectedGroup() { - return isEmpty(this.initialData) ? ANY_OPTION : this.initialData; + return isEmpty(this.groupInitialJson) ? ANY_OPTION : this.groupInitialJson; + }, + }, + watch: { + search() { + this.debounceSearch(); }, }, created() { // This tracks groups searched via the top nav search bar - if (this.query.nav_source === 'navbar' && this.initialData?.id) { - this.setFrequentGroup(this.initialData); + if (this.query.nav_source === 'navbar' && this.groupInitialJson?.id) { + this.setFrequentGroup(this.groupInitialJson); } }, methods: { ...mapActions(['fetchGroups', 'setFrequentGroup', 'loadFrequentGroups']), + firstLoad() { + this.loadFrequentGroups(); + this.fetchGroups(); + }, handleGroupChange(group) { // If group.id is null we are clearing the filter and don't need to store that in LS. if (group.id) { @@ -58,13 +77,13 @@ export default { data-testid="group-filter" :header-text="$options.GROUP_DATA.headerText" :name="$options.GROUP_DATA.name" - :full-name="$options.GROUP_DATA.fullName" :loading="fetchingGroups" :selected-item="selectedGroup" :items="groups" :frequent-items="frequentGroups" - @first-open="loadFrequentGroups" - @search="fetchGroups" + :search-handler="fetchGroups" + :label-id="labelId" + @first-open="firstLoad" @change="handleGroupChange" /> </template> diff --git a/app/assets/javascripts/search/topbar/components/project_filter.vue b/app/assets/javascripts/search/topbar/components/project_filter.vue index c8190b4002d..ecd118a07ac 100644 --- a/app/assets/javascripts/search/topbar/components/project_filter.vue +++ b/app/assets/javascripts/search/topbar/components/project_filter.vue @@ -1,4 +1,5 @@ <script> +import { isEmpty } from 'lodash'; // eslint-disable-next-line no-restricted-imports import { mapState, mapActions, mapGetters } from 'vuex'; import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; @@ -11,27 +12,46 @@ export default { SearchableDropdown, }, props: { - initialData: { + projectInitialJson: { type: Object, required: false, default: () => null, }, + labelId: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + search: '', + }; }, computed: { ...mapState(['query', 'projects', 'fetchingProjects']), ...mapGetters(['frequentProjects', 'currentScope']), selectedProject() { - return this.initialData ? this.initialData : ANY_OPTION; + return isEmpty(this.projectInitialJson) ? ANY_OPTION : this.projectInitialJson; + }, + }, + watch: { + search() { + this.debounceSearch(); }, }, created() { // This tracks projects searched via the top nav search bar - if (this.query.nav_source === 'navbar' && this.initialData?.id) { - this.setFrequentProject(this.initialData); + if (this.query.nav_source === 'navbar' && this.projectInitialJson?.id) { + this.setFrequentProject(this.projectInitialJson); } }, methods: { ...mapActions(['fetchProjects', 'setFrequentProject', 'loadFrequentProjects']), + firstLoad() { + this.loadFrequentProjects(); + this.fetchProjects(); + }, handleProjectChange(project) { // If project.id is null we are clearing the filter and don't need to store that in LS. if (project.id) { @@ -58,13 +78,13 @@ export default { data-testid="project-filter" :header-text="$options.PROJECT_DATA.headerText" :name="$options.PROJECT_DATA.name" - :full-name="$options.PROJECT_DATA.fullName" :loading="fetchingProjects" :selected-item="selectedProject" :items="projects" :frequent-items="frequentProjects" - @first-open="loadFrequentProjects" - @search="fetchProjects" + :search-handler="fetchProjects" + :label-id="labelId" + @first-open="firstLoad" @change="handleProjectChange" /> </template> diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue index ff639d538b3..f4d9de636d4 100644 --- a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue +++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue @@ -1,38 +1,31 @@ <script> -import { - GlDropdown, - GlDropdownItem, - GlDropdownSectionHeader, - GlSearchBoxByType, - GlLoadingIcon, - GlIcon, - GlButton, - GlSkeletonLoader, - GlTooltipDirective, -} from '@gitlab/ui'; -import { __ } from '~/locale'; +import { GlCollapsibleListbox, GlAvatar } from '@gitlab/ui'; +import { debounce } from 'lodash'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import highlight from '~/lib/utils/highlight'; +import { truncateNamespace } from '~/lib/utils/text_utility'; +import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import { __, s__, n__ } from '~/locale'; import { ANY_OPTION } from '../constants'; -import SearchableDropdownItem from './searchable_dropdown_item.vue'; export default { - i18n: { - clearLabel: __('Clear'), - frequentlySearched: __('Frequently searched'), - }, name: 'SearchableDropdown', components: { - GlDropdown, - GlDropdownItem, - GlDropdownSectionHeader, - GlSearchBoxByType, - GlLoadingIcon, - GlIcon, - GlButton, - GlSkeletonLoader, - SearchableDropdownItem, + GlAvatar, + GlCollapsibleListbox, }, directives: { - GlTooltip: GlTooltipDirective, + SafeHtml, + }, + i18n: { + frequentlySearched: __('Frequently searched'), + availableGroups: s__('GlobalSearch|All available groups'), + nothingFound: s__('GlobalSearch|Nothing found…'), + reset: s__('GlobalSearch|Reset'), + itemsFound(count) { + return n__('%d item found', '%d items found', count); + }, }, props: { headerText: { @@ -45,11 +38,6 @@ export default { required: false, default: 'name', }, - fullName: { - type: String, - required: false, - default: 'name', - }, loading: { type: Boolean, required: false, @@ -69,127 +57,167 @@ export default { required: false, default: () => [], }, + searchHandler: { + type: Function, + required: true, + }, + labelId: { + type: String, + required: false, + default: 'labelId', + }, }, data() { return { searchText: '', hasBeenOpened: false, + showableItems: [], + searchInProgress: false, }; }, - computed: { - showFrequentItems() { - return !this.searchText && this.frequentItems.length > 0; + computed: {}, + watch: { + items() { + if (this.searchText === '') { + this.showableItems = this.defaultItems(); + } }, }, + created() { + this.showableItems = this.defaultItems(); + }, methods: { - isSelected(selected) { - return selected.id === this.selectedItem.id; + defaultItems() { + const frequentItems = this.convertItemsFormat([...this.frequentItems]); + const nonFrequentItems = this.convertItemsFormat([ + ...this.uniqueItems(this.items, this.frequentItems), + ]); + + return [ + { + text: '', + options: [ + { + value: ANY_OPTION.name, + text: ANY_OPTION.name, + ...ANY_OPTION, + }, + ], + }, + { + text: this.$options.i18n.frequentlySearched, + options: frequentItems, + }, + { + text: this.$options.i18n.availableGroups, + options: nonFrequentItems, + }, + ].filter((group) => { + return group.options.length > 0; + }); + }, + search(search) { + this.searchText = search; + this.searchInProgress = true; + + if (search !== '') { + debounce(() => { + this.searchHandler(this.searchText); + this.showableItems = this.convertItemsFormat([...this.items]); + }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS)(); + + return; + } + + this.showableItems = this.defaultItems(); }, openDropdown() { if (!this.hasBeenOpened) { this.hasBeenOpened = true; this.$emit('first-open'); } - - this.$emit('search', this.searchText); }, resetDropdown() { this.$emit('change', ANY_OPTION); }, - updateDropdown(item) { - this.$emit('change', item); + convertItemsFormat(items) { + return items.map((item) => ({ value: item.id, text: item.full_name, ...item })); + }, + truncatedNamespace(item) { + const itemDuplicat = { ...item }; + const namespaceWithFallback = itemDuplicat.name_with_namespace + ? itemDuplicat.name_with_namespace + : itemDuplicat.full_name; + + return truncateNamespace(namespaceWithFallback); + }, + highlightedItemName(item) { + return highlight(item.name, item.searchText); + }, + onSelectGroup(selected) { + if (selected === ANY_OPTION.name) { + this.$emit('change', ANY_OPTION); + return; + } + + const flatShowableItems = [...this.frequentItems, ...this.items]; + const newSelectedItem = flatShowableItems.find((item) => item.id === selected); + this.$emit('change', newSelectedItem); + }, + uniqueItems(allItems, frequentItems) { + return allItems.filter((item) => { + const itemNotIdentical = frequentItems.some((fitem) => fitem.id === item.id); + return Boolean(!itemNotIdentical); + }); }, }, ANY_OPTION, + AVATAR_SHAPE_OPTION_RECT, }; </script> <template> - <gl-dropdown - class="gl-w-full" - menu-class="global-search-dropdown-menu" - toggle-class="gl-text-truncate" + <gl-collapsible-listbox + :items="showableItems" :header-text="headerText" - :right="true" - @show="openDropdown" - @shown="$refs.searchBox.focusInput()" + :toggle-text="selectedItem[name]" + :no-results-text="$options.i18n.nothingFound" + :selected="selectedItem.id" + :searching="loading" + :reset-button-label="$options.i18n.reset" + :toggle-aria-labelled-by="labelId" + searchable + block + @shown="openDropdown" + @search="search" + @select="onSelectGroup" + @reset="resetDropdown" > - <template #button-content> - <span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate"> - {{ selectedItem[name] }} - </span> - <gl-loading-icon v-if="loading" size="sm" inline class="gl-mr-3" /> - <gl-button - v-if="!isSelected($options.ANY_OPTION)" - v-gl-tooltip - name="clear" - category="tertiary" - :title="$options.i18n.clearLabel" - :aria-label="$options.i18n.clearLabel" - class="gl-p-0! gl-mr-2" - @keydown.enter.stop="resetDropdown" - @click.stop="resetDropdown" - > - <gl-icon name="clear" /> - </gl-button> - <gl-icon name="chevron-down" /> + <template #search-summary-sr-only> + {{ $options.i18n.itemsFound(showableItems.length) }} + </template> + <template #list-item="{ item }"> + <div class="gl-display-flex gl-align-items-center"> + <gl-avatar + :src="item.avatar_url" + :entity-id="item.id" + :entity-name="item.name" + :shape="$options.AVATAR_SHAPE_OPTION_RECT" + :size="32" + class="gl-mr-3" + aria-hidden="true" + /> + <div class="gl-display-flex gl-flex-direction-column"> + <span + v-safe-html="highlightedItemName(item)" + class="gl-font-weight-bold gl-white-space-nowrap" + data-testid="item-title" + ></span> + <span class="gl-font-sm gl-text-gray-700" data-testid="item-namespace"> + {{ truncatedNamespace(item) }}</span + > + </div> + </div> </template> - <div class="gl-sticky gl-top-0 gl-z-index-1 gl-bg-white"> - <gl-search-box-by-type - ref="searchBox" - v-model="searchText" - class="gl-m-3" - :debounce="500" - @input="openDropdown" - /> - <gl-dropdown-item - class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2" - is-check-item - :is-checked="isSelected($options.ANY_OPTION)" - is-check-centered - @click="updateDropdown($options.ANY_OPTION)" - > - <span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span> - </gl-dropdown-item> - </div> - <div - v-if="showFrequentItems" - class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2 gl-mb-2" - > - <gl-dropdown-section-header>{{ - $options.i18n.frequentlySearched - }}</gl-dropdown-section-header> - <searchable-dropdown-item - v-for="item in frequentItems" - :key="item.id" - :item="item" - :selected-item="selectedItem" - :search-text="searchText" - :name="name" - :full-name="fullName" - data-testid="frequent-items" - @change="updateDropdown" - /> - </div> - <div v-if="!loading"> - <searchable-dropdown-item - v-for="item in items" - :key="item.id" - :item="item" - :selected-item="selectedItem" - :search-text="searchText" - :name="name" - :full-name="fullName" - data-testid="searchable-items" - @change="updateDropdown" - /> - </div> - <div v-if="loading" class="gl-mx-4 gl-mt-3"> - <gl-skeleton-loader :height="100"> - <rect y="0" width="90%" height="20" rx="4" /> - <rect y="40" width="70%" height="20" rx="4" /> - <rect y="80" width="80%" height="20" rx="4" /> - </gl-skeleton-loader> - </div> - </gl-dropdown> + </gl-collapsible-listbox> </template> diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue deleted file mode 100644 index c1e33df3c42..00000000000 --- a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue +++ /dev/null @@ -1,78 +0,0 @@ -<script> -import { GlDropdownItem, GlAvatar } from '@gitlab/ui'; -import SafeHtml from '~/vue_shared/directives/safe_html'; -import highlight from '~/lib/utils/highlight'; -import { truncateNamespace } from '~/lib/utils/text_utility'; -import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; - -export default { - name: 'SearchableDropdownItem', - components: { - GlDropdownItem, - GlAvatar, - }, - directives: { - SafeHtml, - }, - props: { - item: { - type: Object, - required: true, - }, - selectedItem: { - type: Object, - required: true, - }, - searchText: { - type: String, - required: false, - default: '', - }, - name: { - type: String, - required: true, - }, - fullName: { - type: String, - required: true, - }, - }, - computed: { - isSelected() { - return this.item.id === this.selectedItem.id; - }, - truncatedNamespace() { - return truncateNamespace(this.item[this.fullName]); - }, - highlightedItemName() { - return highlight(this.item[this.name], this.searchText); - }, - }, - AVATAR_SHAPE_OPTION_RECT, -}; -</script> - -<template> - <gl-dropdown-item - is-check-item - :is-checked="isSelected" - is-check-centered - @click="$emit('change', item)" - > - <div class="gl-display-flex gl-align-items-center"> - <gl-avatar - :src="item.avatar_url" - :entity-id="item.id" - :entity-name="item[name]" - :shape="$options.AVATAR_SHAPE_OPTION_RECT" - :size="32" - /> - <div class="gl-display-flex gl-flex-direction-column"> - <span v-safe-html="highlightedItemName" data-testid="item-title"></span> - <span class="gl-font-sm gl-text-gray-700" data-testid="item-namespace">{{ - truncatedNamespace - }}</span> - </div> - </div> - </gl-dropdown-item> -</template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql index a5f3f348cfc..c497224cde3 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql +++ b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql @@ -14,6 +14,7 @@ query getBlameData($fullPath: ID!, $filePath: String!, $fromLine: Int, $toLine: span commit { id + authorName titleHtml message authoredDate diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ef87fedf538..b8b79192d3f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -397,6 +397,7 @@ class ProjectsController < Projects::ApplicationController if can?(current_user, :read_code, @project) return render 'projects/no_repo' unless @project.repository_exists? + return render 'projects/missing_default_branch', status: :service_unavailable if @ref == '' render 'projects/empty' if @project.empty_repo? else @@ -553,6 +554,9 @@ class ProjectsController < Projects::ApplicationController # Override get_id from ExtractsPath in this case is just the root of the default branch. def get_id project.repository.root_ref + rescue Gitlab::Git::CommandError + # Empty string is intentional and prevent the @ref reload + '' end def build_canonical_path(project) diff --git a/app/views/projects/missing_default_branch.html.haml b/app/views/projects/missing_default_branch.html.haml new file mode 100644 index 00000000000..66a466d8890 --- /dev/null +++ b/app/views/projects/missing_default_branch.html.haml @@ -0,0 +1,10 @@ +- @skip_current_level_breadcrumb = true + += render Pajamas::AlertComponent.new(alert_options: { class: 'gl-my-5' }, + variant: :danger, + dismissible: false, + title: s_('ProjectPage|Unable to load default branch')) do |c| + - c.with_body do + = s_('ProjectPage|The default branch was not able to be found. Please contact your administrator.') + += render 'home_panel' |