diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-21 15:09:05 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-21 15:09:05 +0300 |
commit | cbf0385d17af276030868aa260af02f3f90ee683 (patch) | |
tree | f2953cb7d17d319ee917c67937d79f341c3be3d0 /app | |
parent | ea037b91577f1b645267df9e034f6da3e389626c (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
23 files changed, 310 insertions, 218 deletions
diff --git a/app/assets/javascripts/groups/members/constants.js b/app/assets/javascripts/groups/members/constants.js index 6d71b666d7a..3315712891d 100644 --- a/app/assets/javascripts/groups/members/constants.js +++ b/app/assets/javascripts/groups/members/constants.js @@ -1,5 +1 @@ export const GROUP_MEMBER_BASE_PROPERTY_NAME = 'group_member'; -export const GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME = 'access_level'; - -export const GROUP_LINK_BASE_PROPERTY_NAME = 'group_link'; -export const GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME = 'group_access'; diff --git a/app/assets/javascripts/groups/members/utils.js b/app/assets/javascripts/groups/members/utils.js index 4fcf348b69f..71918bfe9f0 100644 --- a/app/assets/javascripts/groups/members/utils.js +++ b/app/assets/javascripts/groups/members/utils.js @@ -1,45 +1,8 @@ -import { isUndefined } from 'lodash'; -import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; -import { - GROUP_MEMBER_BASE_PROPERTY_NAME, - GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME, - GROUP_LINK_BASE_PROPERTY_NAME, - GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME, -} from './constants'; - -export const parseDataAttributes = (el) => { - const { members, groupId, memberPath, canManageMembers } = el.dataset; - - return { - members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }), - sourceId: parseInt(groupId, 10), - memberPath, - canManageMembers: parseBoolean(canManageMembers), - }; -}; - -const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({ - accessLevel, - ...otherProperties -}) => { - const accessLevelProperty = !isUndefined(accessLevel) - ? { [accessLevelPropertyName]: accessLevel } - : {}; +import { baseRequestFormatter } from '~/members/utils'; +import { MEMBER_ACCESS_LEVEL_PROPERTY_NAME } from '~/members/constants'; +import { GROUP_MEMBER_BASE_PROPERTY_NAME } from './constants'; - return { - [basePropertyName]: { - ...accessLevelProperty, - ...otherProperties, - }, - }; -}; - -export const memberRequestFormatter = baseRequestFormatter( +export const groupMemberRequestFormatter = baseRequestFormatter( GROUP_MEMBER_BASE_PROPERTY_NAME, - GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME, -); - -export const groupLinkRequestFormatter = baseRequestFormatter( - GROUP_LINK_BASE_PROPERTY_NAME, - GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME, + MEMBER_ACCESS_LEVEL_PROPERTY_NAME, ); diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index fb4785a6f8c..0d24ab1f01e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -1,13 +1,17 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; -import { GlModal, GlSafeHtmlDirective, GlButton } from '@gitlab/ui'; -import { n__ } from '~/locale'; +import { GlModal, GlSafeHtmlDirective, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { n__, s__ } from '~/locale'; import CommitMessageField from './message_field.vue'; import Actions from './actions.vue'; import SuccessMessage from './success_message.vue'; import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; import { createUnexpectedCommitError } from '../../lib/errors'; +const MSG_CANNOT_PUSH_CODE = s__( + 'WebIDE|You need permission to edit files directly in this project.', +); + export default { components: { Actions, @@ -18,6 +22,7 @@ export default { }, directives: { SafeHtml: GlSafeHtmlDirective, + GlTooltip: GlTooltipDirective, }, data() { return { @@ -30,8 +35,18 @@ export default { computed: { ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']), ...mapState('commit', ['commitMessage', 'submitCommitLoading', 'commitError']), - ...mapGetters(['someUncommittedChanges']), + ...mapGetters(['someUncommittedChanges', 'canPushCode']), ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']), + commitButtonDisabled() { + return !this.canPushCode || !this.someUncommittedChanges; + }, + commitButtonTooltip() { + if (!this.canPushCode) { + return MSG_CANNOT_PUSH_CODE; + } + + return ''; + }, overviewText() { return n__('%d changed file', '%d changed files', this.stagedFiles.length); }, @@ -69,6 +84,12 @@ export default { 'updateCommitAction', ]), commit() { + // Even though the submit button will be disabled, we need to disable the submission + // since hitting enter on the branch name text input also submits the form. + if (!this.canPushCode) { + return false; + } + return this.commitChanges(); }, handleCompactState() { @@ -109,6 +130,8 @@ export default { this.componentHeight = null; }, }, + // Expose for tests + MSG_CANNOT_PUSH_CODE, }; </script> @@ -130,17 +153,22 @@ export default { @after-enter="afterEndTransition" > <div v-if="isCompact" ref="compactEl" class="commit-form-compact"> - <gl-button - :disabled="!someUncommittedChanges" - category="primary" - variant="info" - block - class="qa-begin-commit-button" - data-testid="begin-commit-button" - @click="beginCommit" + <div + v-gl-tooltip="{ title: commitButtonTooltip }" + data-testid="begin-commit-button-tooltip" > - {{ __('Commit…') }} - </gl-button> + <gl-button + :disabled="commitButtonDisabled" + category="primary" + variant="info" + block + class="qa-begin-commit-button" + data-testid="begin-commit-button" + @click="beginCommit" + > + {{ __('Commit…') }} + </gl-button> + </div> <p class="text-center bold">{{ overviewText }}</p> </div> <form v-else ref="formEl" @submit.prevent.stop="commit"> @@ -153,16 +181,23 @@ export default { /> <div class="clearfix gl-mt-5"> <actions /> - <gl-button - :loading="submitCommitLoading" - class="float-left qa-commit-button" - data-testid="commit-button" - category="primary" - variant="success" - @click="commit" + <div + v-gl-tooltip="{ title: commitButtonTooltip }" + class="float-left" + data-testid="commit-button-tooltip" > - {{ __('Commit') }} - </gl-button> + <gl-button + :disabled="commitButtonDisabled" + :loading="submitCommitLoading" + data-testid="commit-button" + class="qa-commit-button" + category="primary" + variant="success" + @click="commit" + > + {{ __('Commit') }} + </gl-button> + </div> <gl-button v-if="!discardDraftButtonDisabled" class="float-right" diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index aac899fde0d..644c79f2aab 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,7 +1,7 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlButton, GlLoadingIcon } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; import { WEBIDE_MARK_APP_START, WEBIDE_MARK_FILE_FINISH, @@ -26,10 +26,15 @@ eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () => ), ); +const MSG_CANNOT_PUSH_CODE = s__( + 'WebIDE|You need permission to edit files directly in this project. Fork this project to make your changes and submit a merge request.', +); + export default { components: { IdeSidebar, RepoEditor, + GlAlert, GlButton, GlLoadingIcon, ErrorMessage: () => import(/* webpackChunkName: 'ide_runtime' */ './error_message.vue'), @@ -59,12 +64,14 @@ export default { 'loading', ]), ...mapGetters([ + 'canPushCode', 'activeFile', 'someUncommittedChanges', 'isCommitModeActive', 'allBlobs', 'emptyRepo', 'currentTree', + 'hasCurrentProject', 'editorTheme', 'getUrlForPath', ]), @@ -110,6 +117,7 @@ export default { this.loadDeferred = true; }, }, + MSG_CANNOT_PUSH_CODE, }; </script> @@ -118,6 +126,9 @@ export default { class="ide position-relative d-flex flex-column align-items-stretch" :class="{ [`theme-${themeName}`]: themeName }" > + <gl-alert v-if="!canPushCode" :dismissible="false">{{ + $options.MSG_CANNOT_PUSH_CODE + }}</gl-alert> <error-message v-if="errorMessage" :message="errorMessage" /> <div class="ide-view flex-grow d-flex"> <template v-if="loadDeferred"> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index e5618466395..6bd74b143e2 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -16,6 +16,13 @@ export const PERMISSION_CREATE_MR = 'createMergeRequestIn'; export const PERMISSION_READ_MR = 'readMergeRequest'; export const PERMISSION_PUSH_CODE = 'pushCode'; +// The default permission object to use when the project data isn't available yet. +// This helps us encapsulate checks like `canPushCode` without requiring an +// additional check like `currentProject && canPushCode`. +export const DEFAULT_PERMISSIONS = { + [PERMISSION_PUSH_CODE]: true, +}; + export const viewerTypes = { mr: 'mrdiff', edit: 'editor', diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 59e8d37a92a..697fa02bcb4 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -2,6 +2,7 @@ import { getChangesCountForFiles, filePathMatches } from './utils'; import { leftSidebarViews, packageJsonPath, + DEFAULT_PERMISSIONS, PERMISSION_READ_MR, PERMISSION_CREATE_MR, PERMISSION_PUSH_CODE, @@ -150,7 +151,7 @@ export const getDiffInfo = (state, getters) => (path) => { }; export const findProjectPermissions = (state, getters) => (projectId) => - getters.findProject(projectId)?.userPermissions || {}; + getters.findProject(projectId)?.userPermissions || DEFAULT_PERMISSIONS; export const canReadMergeRequests = (state, getters) => Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_READ_MR]); diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/members/components/app.vue index 34a2c67fa9f..c08c36d1b30 100644 --- a/app/assets/javascripts/groups/members/components/app.vue +++ b/app/assets/javascripts/members/components/app.vue @@ -1,13 +1,13 @@ <script> import { mapState, mapMutations } from 'vuex'; import { GlAlert } from '@gitlab/ui'; -import MembersTable from '~/members/components/table/members_table.vue'; -import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue'; +import MembersTable from './table/members_table.vue'; +import FilterSortContainer from './filter_sort/filter_sort_container.vue'; +import { HIDE_ERROR } from '../store/mutation_types'; import { scrollToElement } from '~/lib/utils/common_utils'; -import { HIDE_ERROR } from '~/members/store/mutation_types'; export default { - name: 'GroupMembersApp', + name: 'MembersApp', components: { MembersTable, FilterSortContainer, GlAlert }, computed: { ...mapState(['showError', 'errorMessage']), diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js index 77cb150bff6..f68a8814fee 100644 --- a/app/assets/javascripts/members/constants.js +++ b/app/assets/javascripts/members/constants.js @@ -98,3 +98,8 @@ export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id'; export const SEARCH_TOKEN_TYPE = 'filtered-search-term'; export const SORT_PARAM = 'sort'; + +export const MEMBER_ACCESS_LEVEL_PROPERTY_NAME = 'access_level'; + +export const GROUP_LINK_BASE_PROPERTY_NAME = 'group_link'; +export const GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME = 'group_access'; diff --git a/app/assets/javascripts/groups/members/index.js b/app/assets/javascripts/members/index.js index 3ec874b8d36..bd80bb2485b 100644 --- a/app/assets/javascripts/groups/members/index.js +++ b/app/assets/javascripts/members/index.js @@ -1,11 +1,11 @@ import Vue from 'vue'; import Vuex from 'vuex'; import { GlToast } from '@gitlab/ui'; -import { parseDataAttributes } from 'ee_else_ce/groups/members/utils'; +import { parseDataAttributes } from 'ee_else_ce/members/utils'; import App from './components/app.vue'; -import membersStore from '~/members/store'; +import membersStore from './store'; -export const initGroupMembersApp = ( +export const initMembersApp = ( el, { tableFields = [], diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js index 780b5a9df57..723b371ccb0 100644 --- a/app/assets/javascripts/members/utils.js +++ b/app/assets/javascripts/members/utils.js @@ -1,7 +1,17 @@ +import { isUndefined } from 'lodash'; import { __ } from '~/locale'; -import { getParameterByName } from '~/lib/utils/common_utils'; +import { + getParameterByName, + convertObjectPropsToCamelCase, + parseBoolean, +} from '~/lib/utils/common_utils'; import { setUrlParams } from '~/lib/utils/url_utility'; -import { FIELDS, DEFAULT_SORT } from './constants'; +import { + FIELDS, + DEFAULT_SORT, + GROUP_LINK_BASE_PROPERTY_NAME, + GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME, +} from './constants'; export const generateBadges = (member, isCurrentUser) => [ { @@ -95,3 +105,35 @@ export const buildSortHref = ({ // Defined in `ee/app/assets/javascripts/vue_shared/components/members/utils.js` export const canOverride = () => false; + +export const parseDataAttributes = (el) => { + const { members, sourceId, memberPath, canManageMembers } = el.dataset; + + return { + members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }), + sourceId: parseInt(sourceId, 10), + memberPath, + canManageMembers: parseBoolean(canManageMembers), + }; +}; + +export const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({ + accessLevel, + ...otherProperties +}) => { + const accessLevelProperty = !isUndefined(accessLevel) + ? { [accessLevelPropertyName]: accessLevel } + : {}; + + return { + [basePropertyName]: { + ...accessLevelProperty, + ...otherProperties, + }, + }; +}; + +export const groupLinkRequestFormatter = baseRequestFormatter( + GROUP_LINK_BASE_PROPERTY_NAME, + GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME, +); diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index 5346e3720e8..516311dd841 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -3,10 +3,11 @@ import memberExpirationDate from '~/member_expiration_date'; import UsersSelect from '~/users_select'; import groupsSelect from '~/groups_select'; import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; -import { initGroupMembersApp } from '~/groups/members'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; -import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils'; +import { initMembersApp } from '~/members/index'; +import { groupMemberRequestFormatter } from '~/groups/members/utils'; +import { groupLinkRequestFormatter } from '~/members/utils'; import { s__ } from '~/locale'; function mountRemoveMemberModal() { @@ -25,11 +26,11 @@ function mountRemoveMemberModal() { const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions']; -initGroupMembersApp(document.querySelector('.js-group-members-list'), { +initMembersApp(document.querySelector('.js-group-members-list'), { tableFields: SHARED_FIELDS.concat(['source', 'granted']), tableAttrs: { tr: { 'data-qa-selector': 'member_row' } }, tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'], - requestFormatter: memberRequestFormatter, + requestFormatter: groupMemberRequestFormatter, filteredSearchBar: { show: true, tokens: ['two_factor', 'with_inherited_permissions'], @@ -38,7 +39,8 @@ initGroupMembersApp(document.querySelector('.js-group-members-list'), { recentSearchesStorageKey: 'group_members', }, }); -initGroupMembersApp(document.querySelector('.js-group-linked-list'), { + +initMembersApp(document.querySelector('.js-group-group-links-list'), { tableFields: SHARED_FIELDS.concat('granted'), tableAttrs: { table: { 'data-qa-selector': 'groups_list' }, @@ -46,9 +48,9 @@ initGroupMembersApp(document.querySelector('.js-group-linked-list'), { }, requestFormatter: groupLinkRequestFormatter, }); -initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), { +initMembersApp(document.querySelector('.js-group-invited-members-list'), { tableFields: SHARED_FIELDS.concat('invited'), - requestFormatter: memberRequestFormatter, + requestFormatter: groupMemberRequestFormatter, filteredSearchBar: { show: true, tokens: [], @@ -57,9 +59,9 @@ initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), { recentSearchesStorageKey: 'group_invited_members', }, }); -initGroupMembersApp(document.querySelector('.js-group-access-requests-list'), { +initMembersApp(document.querySelector('.js-group-access-requests-list'), { tableFields: SHARED_FIELDS.concat('requested'), - requestFormatter: memberRequestFormatter, + requestFormatter: groupMemberRequestFormatter, }); groupsSelect(); diff --git a/app/assets/javascripts/performance_bar/performance_bar_log.js b/app/assets/javascripts/performance_bar/performance_bar_log.js index c61b0cb32e8..aad99e2604e 100644 --- a/app/assets/javascripts/performance_bar/performance_bar_log.js +++ b/app/assets/javascripts/performance_bar/performance_bar_log.js @@ -43,7 +43,7 @@ const logUserTimingMetrics = () => { const initPerformanceBarLog = () => { console.log( `%c ${String.fromCodePoint(0x1f98a)} GitLab performance bar`, - 'width:100%;background-color: #292961; color: #FFFFFF; font-size:24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; padding: 10px;display:block;padding-right: 100px;', + 'width:100%; background-color: #292961; color: #FFFFFF; padding: 10px; display:block;', ); initVitalsLog(); diff --git a/app/assets/stylesheets/page_bundles/oncall_schedules.scss b/app/assets/stylesheets/page_bundles/oncall_schedules.scss index 1b190024457..a1d1ff0c42f 100644 --- a/app/assets/stylesheets/page_bundles/oncall_schedules.scss +++ b/app/assets/stylesheets/page_bundles/oncall_schedules.scss @@ -37,12 +37,6 @@ &.gl-modal .modal-md { max-width: 640px; } - - // TODO: move to gitlab/ui utilities - // https://gitlab.com/gitlab-org/gitlab/-/issues/297502 - .gl-w-fit-content { - width: fit-content; - } } //// Copied from roadmaps.scss - adapted for on-call schedules diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb index a4159ed6b19..76f31dc97cb 100644 --- a/app/helpers/groups/group_members_helper.rb +++ b/app/helpers/groups/group_members_helper.rb @@ -13,7 +13,7 @@ module Groups::GroupMembersHelper render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: group.access_level_roles, default_access_level: default_access_level end - def linked_groups_data_json(group_links) + def group_group_links_data_json(group_links) GroupGroupLinkSerializer.new.represent(group_links, { current_user: current_user }).to_json end @@ -26,16 +26,16 @@ module Groups::GroupMembersHelper { members: members_data_json(group, members), member_path: group_group_member_path(group, ':id'), - group_id: group.id, + source_id: group.id, can_manage_members: can?(current_user, :admin_group_member, group).to_s } end - def linked_groups_list_data_attributes(group) + def group_group_links_list_data_attributes(group) { - members: linked_groups_data_json(group.shared_with_group_links), + members: group_group_links_data_json(group.shared_with_group_links), member_path: group_group_link_path(group, ':id'), - group_id: group.id + source_id: group.id } end end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 56acac53e0b..f87eccecf9f 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ApplicationRecord - VERSION = '0.24.0' + VERSION = '0.25.0' self.table_name = 'clusters_applications_runners' diff --git a/app/services/projects/cleanup_service.rb b/app/services/projects/cleanup_service.rb index 6e3b320afbe..7bcaee75813 100644 --- a/app/services/projects/cleanup_service.rb +++ b/app/services/projects/cleanup_service.rb @@ -40,7 +40,7 @@ module Projects apply_bfg_object_map! # Remove older objects that are no longer referenced - GitGarbageCollectWorker.new.perform(project.id, :prune, "project_cleanup:gc:#{project.id}") + Projects::GitGarbageCollectWorker.new.perform(project.id, :prune, "project_cleanup:gc:#{project.id}") # The cache may now be inaccurate, and holding onto it could prevent # bugs assuming the presence of some object from manifesting for some diff --git a/app/services/repositories/housekeeping_service.rb b/app/services/repositories/housekeeping_service.rb index 6a2fa95d25f..e97c295e18e 100644 --- a/app/services/repositories/housekeeping_service.rb +++ b/app/services/repositories/housekeeping_service.rb @@ -45,7 +45,7 @@ module Repositories private def execute_gitlab_shell_gc(lease_uuid) - GitGarbageCollectWorker.perform_async(@resource.id, task, lease_key, lease_uuid) + Projects::GitGarbageCollectWorker.perform_async(@resource.id, task, lease_key, lease_uuid) ensure if pushes_since_gc >= gc_period Gitlab::Metrics.measure(:reset_pushes_since_gc) do diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index abdb4553cdd..a5257ff20bc 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -66,7 +66,7 @@ = paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil } - if @group.shared_with_group_links.any? #tab-groups.tab-pane - .js-group-linked-list{ data: linked_groups_list_data_attributes(@group) } + .js-group-group-links-list{ data: group_group_links_list_data_attributes(@group) } .loading .spinner.spinner-md - if show_invited_members diff --git a/app/views/layouts/welcome.html.haml b/app/views/layouts/welcome.html.haml index 48921e9ff89..30ba7f7f230 100644 --- a/app/views/layouts/welcome.html.haml +++ b/app/views/layouts/welcome.html.haml @@ -1,7 +1,7 @@ !!! 5 %html.subscriptions-layout-html{ lang: 'en' } = render 'layouts/head' - %body.ui-indigo.d-flex.vh-100.gl-bg-gray-10 + %body.ui-indigo.gl-display-flex.vh-100 = render "layouts/header/logo_with_title" = render "layouts/broadcast" .container.d-flex.flex-grow-1.m-0 diff --git a/app/views/projects/_merge_request_merge_options_settings.html.haml b/app/views/projects/_merge_request_merge_options_settings.html.haml index 8951f2ed22f..80dabeceeb0 100644 --- a/app/views/projects/_merge_request_merge_options_settings.html.haml +++ b/app/views/projects/_merge_request_merge_options_settings.html.haml @@ -1,6 +1,6 @@ - form = local_assigns.fetch(:form) -.form-group +.form-group#project-merge-options{ data: { project_full_path: @project.full_path } } %b= s_('ProjectSettings|Merge options') %p.text-secondary= s_('ProjectSettings|Additional merge request capabilities that influence how and when merges will be performed') = render_if_exists 'projects/merge_pipelines_settings', form: form diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 4c4a314a1e6..8590e1bc6f7 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1999,6 +1999,14 @@ :weight: 1 :idempotent: true :tags: [] +- :name: projects_git_garbage_collect + :feature_category: :gitaly + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: + :tags: [] - :name: prometheus_create_default_alerts :feature_category: :incident_management :has_external_dependencies: diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index e1dcb16bafb..a2aab23db7b 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true +# According to our docs, we can only remove workers on major releases +# https://docs.gitlab.com/ee/development/sidekiq_style_guide.html#removing-workers. +# +# We need to still maintain this until 14.0 but with the current functionality. +# +# In https://gitlab.com/gitlab-org/gitlab/-/issues/299290 we track that removal. class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker include ApplicationWorker @@ -7,117 +13,7 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker feature_category :gitaly loggable_arguments 1, 2, 3 - # Timeout set to 24h - LEASE_TIMEOUT = 86400 - def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil) - lease_key ||= "git_gc:#{task}:#{project_id}" - project = Project.find(project_id) - active_uuid = get_lease_uuid(lease_key) - - if active_uuid - return unless active_uuid == lease_uuid - - renew_lease(lease_key, active_uuid) - else - lease_uuid = try_obtain_lease(lease_key) - - return unless lease_uuid - end - - task = task.to_sym - - if gc?(task) - ::Projects::GitDeduplicationService.new(project).execute - cleanup_orphan_lfs_file_references(project) - end - - gitaly_call(task, project) - - # Refresh the branch cache in case garbage collection caused a ref lookup to fail - flush_ref_caches(project) if gc?(task) - - 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 - project.cleanup - ensure - cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present? - end - - private - - def gc?(task) - task == :gc || task == :prune - end - - def try_obtain_lease(key) - ::Gitlab::ExclusiveLease.new(key, timeout: LEASE_TIMEOUT).try_obtain - end - - def renew_lease(key, uuid) - ::Gitlab::ExclusiveLease.new(key, uuid: uuid, timeout: LEASE_TIMEOUT).renew - end - - def cancel_lease(key, uuid) - ::Gitlab::ExclusiveLease.cancel(key, uuid) - end - - def get_lease_uuid(key) - ::Gitlab::ExclusiveLease.get_uuid(key) - end - - def gitaly_call(task, project) - repository = project.repository.raw_repository - - client = if task == :pack_refs - Gitlab::GitalyClient::RefService.new(repository) - else - Gitlab::GitalyClient::RepositoryService.new(repository) - end - - case task - when :prune, :gc - client.garbage_collect(bitmaps_enabled?, prune: task == :prune) - when :full_repack - client.repack_full(bitmaps_enabled?) - when :incremental_repack - client.repack_incremental - when :pack_refs - client.pack_refs - end - rescue GRPC::NotFound => e - Gitlab::GitLogger.error("#{__method__} failed:\nRepository not found") - raise Gitlab::Git::Repository::NoRepository.new(e) - rescue GRPC::BadStatus => e - Gitlab::GitLogger.error("#{__method__} failed:\n#{e}") - raise Gitlab::Git::CommandError.new(e) - end - - def cleanup_orphan_lfs_file_references(project) - return if Gitlab::Database.read_only? # GitGarbageCollectWorker may be run on a Geo secondary - - ::Gitlab::Cleanup::OrphanLfsFileReferences.new(project, dry_run: false, logger: logger).run! - rescue => err - Gitlab::GitLogger.warn(message: "Cleaning up orphan LFS objects files failed", error: err.message) - Gitlab::ErrorTracking.track_and_raise_for_dev_exception(err) - end - - def flush_ref_caches(project) - project.repository.expire_branches_cache - project.repository.branch_names - 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 + ::Projects::GitGarbageCollectWorker.new.perform(project_id, task, lease_key, lease_uuid) end end diff --git a/app/workers/projects/git_garbage_collect_worker.rb b/app/workers/projects/git_garbage_collect_worker.rb new file mode 100644 index 00000000000..aba99ce35d0 --- /dev/null +++ b/app/workers/projects/git_garbage_collect_worker.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +module Projects + class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + + sidekiq_options retry: false + feature_category :gitaly + loggable_arguments 1, 2, 3 + + # Timeout set to 24h + LEASE_TIMEOUT = 86400 + + def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil) + lease_key ||= "git_gc:#{task}:#{project_id}" + project = find_project(project_id) + active_uuid = get_lease_uuid(lease_key) + + if active_uuid + return unless active_uuid == lease_uuid + + renew_lease(lease_key, active_uuid) + else + lease_uuid = try_obtain_lease(lease_key) + + return unless lease_uuid + end + + task = task.to_sym + + if gc?(task) + ::Projects::GitDeduplicationService.new(project).execute + cleanup_orphan_lfs_file_references(project) + end + + gitaly_call(task, project) + + # Refresh the branch cache in case garbage collection caused a ref lookup to fail + flush_ref_caches(project) if gc?(task) + + 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 + project.cleanup + ensure + cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present? + end + + private + + def find_project(project_id) + Project.find(project_id) + end + + def gc?(task) + task == :gc || task == :prune + end + + def try_obtain_lease(key) + ::Gitlab::ExclusiveLease.new(key, timeout: LEASE_TIMEOUT).try_obtain + end + + def renew_lease(key, uuid) + ::Gitlab::ExclusiveLease.new(key, uuid: uuid, timeout: LEASE_TIMEOUT).renew + end + + def cancel_lease(key, uuid) + ::Gitlab::ExclusiveLease.cancel(key, uuid) + end + + def get_lease_uuid(key) + ::Gitlab::ExclusiveLease.get_uuid(key) + end + + def gitaly_call(task, project) + repository = project.repository.raw_repository + client = get_gitaly_client(task, repository) + + case task + when :prune, :gc + client.garbage_collect(bitmaps_enabled?, prune: task == :prune) + when :full_repack + client.repack_full(bitmaps_enabled?) + when :incremental_repack + client.repack_incremental + when :pack_refs + client.pack_refs + end + rescue GRPC::NotFound => e + Gitlab::GitLogger.error("#{__method__} failed:\nRepository not found") + raise Gitlab::Git::Repository::NoRepository.new(e) + rescue GRPC::BadStatus => e + Gitlab::GitLogger.error("#{__method__} failed:\n#{e}") + raise Gitlab::Git::CommandError.new(e) + end + + def get_gitaly_client(task, repository) + if task == :pack_refs + Gitlab::GitalyClient::RefService + else + Gitlab::GitalyClient::RepositoryService + end.new(repository) + end + + def cleanup_orphan_lfs_file_references(project) + return if Gitlab::Database.read_only? # GitGarbageCollectWorker may be run on a Geo secondary + + ::Gitlab::Cleanup::OrphanLfsFileReferences.new(project, dry_run: false, logger: logger).run! + rescue => err + Gitlab::GitLogger.warn(message: "Cleaning up orphan LFS objects files failed", error: err.message) + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(err) + end + + def flush_ref_caches(project) + project.repository.expire_branches_cache + project.repository.branch_names + 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 + end +end |