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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-01-21 15:09:05 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-01-21 15:09:05 +0300
commitcbf0385d17af276030868aa260af02f3f90ee683 (patch)
treef2953cb7d17d319ee917c67937d79f341c3be3d0
parentea037b91577f1b645267df9e034f6da3e389626c (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_manual_todo.yml1
-rw-r--r--app/assets/javascripts/groups/members/constants.js4
-rw-r--r--app/assets/javascripts/groups/members/utils.js47
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue79
-rw-r--r--app/assets/javascripts/ide/components/ide.vue15
-rw-r--r--app/assets/javascripts/ide/constants.js7
-rw-r--r--app/assets/javascripts/ide/stores/getters.js3
-rw-r--r--app/assets/javascripts/members/components/app.vue (renamed from app/assets/javascripts/groups/members/components/app.vue)8
-rw-r--r--app/assets/javascripts/members/constants.js5
-rw-r--r--app/assets/javascripts/members/index.js (renamed from app/assets/javascripts/groups/members/index.js)6
-rw-r--r--app/assets/javascripts/members/utils.js46
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js20
-rw-r--r--app/assets/javascripts/performance_bar/performance_bar_log.js2
-rw-r--r--app/assets/stylesheets/page_bundles/oncall_schedules.scss6
-rw-r--r--app/helpers/groups/group_members_helper.rb10
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/services/projects/cleanup_service.rb2
-rw-r--r--app/services/repositories/housekeeping_service.rb2
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/layouts/welcome.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_options_settings.html.haml2
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/git_garbage_collect_worker.rb118
-rw-r--r--app/workers/projects/git_garbage_collect_worker.rb132
-rw-r--r--changelogs/unreleased/213581-ide-alert-when-cannot-push-code.yml5
-rw-r--r--changelogs/unreleased/229174_use_unix_time_for_jira_update_sequence_id.yml5
-rw-r--r--changelogs/unreleased/fj-add-repository-read-only-to-groups.yml5
-rw-r--r--changelogs/unreleased/revert-3c18ef0b.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-25-0.yml5
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20210115084949_add_repository_read_only_to_groups.rb19
-rw-r--r--db/migrate/20210121093618_remove_repository_read_only_to_groups.rb23
-rw-r--r--db/schema_migrations/202101150849491
-rw-r--r--db/schema_migrations/202101210936181
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/housekeeping.md2
-rw-r--r--doc/api/boards.md6
-rw-r--r--doc/api/epic_links.md3
-rw-r--r--doc/user/project/issues/issue_data_and_actions.md2
-rw-r--r--lib/atlassian/jira_connect/client.rb2
-rw-r--r--locale/gitlab.pot6
-rwxr-xr-xscripts/api/get_job_id.rb4
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb2
-rw-r--r--spec/frontend/groups/members/mock_data.js33
-rw-r--r--spec/frontend/groups/members/utils_spec.js45
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js57
-rw-r--r--spec/frontend/ide/components/ide_spec.js56
-rw-r--r--spec/frontend/ide/stores/getters_spec.js5
-rw-r--r--spec/frontend/members/components/app_spec.js (renamed from spec/frontend/groups/members/components/app_spec.js)6
-rw-r--r--spec/frontend/members/index_spec.js (renamed from spec/frontend/groups/members/index_spec.js)20
-rw-r--r--spec/frontend/members/mock_data.js2
-rw-r--r--spec/frontend/members/utils_spec.js38
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb14
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb14
-rw-r--r--spec/services/projects/cleanup_service_spec.rb2
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb16
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb346
-rw-r--r--spec/workers/projects/git_garbage_collect_worker_spec.rb374
61 files changed, 935 insertions, 733 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index d88d705a012..5be03fa4b1b 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -1435,7 +1435,6 @@ RSpec/AnyInstanceOf:
- 'spec/workers/emails_on_push_worker_spec.rb'
- 'spec/workers/error_tracking_issue_link_worker_spec.rb'
- 'spec/workers/expire_pipeline_cache_worker_spec.rb'
- - 'spec/workers/git_garbage_collect_worker_spec.rb'
- 'spec/workers/group_export_worker_spec.rb'
- 'spec/workers/group_import_worker_spec.rb'
- 'spec/workers/namespaceless_project_destroy_worker_spec.rb'
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
diff --git a/changelogs/unreleased/213581-ide-alert-when-cannot-push-code.yml b/changelogs/unreleased/213581-ide-alert-when-cannot-push-code.yml
new file mode 100644
index 00000000000..71618431dec
--- /dev/null
+++ b/changelogs/unreleased/213581-ide-alert-when-cannot-push-code.yml
@@ -0,0 +1,5 @@
+---
+title: Web IDE shows alert and disable buttons when user cannot push code
+merge_request: 51710
+author:
+type: changed
diff --git a/changelogs/unreleased/229174_use_unix_time_for_jira_update_sequence_id.yml b/changelogs/unreleased/229174_use_unix_time_for_jira_update_sequence_id.yml
new file mode 100644
index 00000000000..5d51d8c99ea
--- /dev/null
+++ b/changelogs/unreleased/229174_use_unix_time_for_jira_update_sequence_id.yml
@@ -0,0 +1,5 @@
+---
+title: Change Jira Connect update sequence id to use Unix Time
+merge_request: 51697
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-add-repository-read-only-to-groups.yml b/changelogs/unreleased/fj-add-repository-read-only-to-groups.yml
deleted file mode 100644
index ba2dcd887e7..00000000000
--- a/changelogs/unreleased/fj-add-repository-read-only-to-groups.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add repository_read_only column to the Namespace table
-merge_request: 51800
-author:
-type: added
diff --git a/changelogs/unreleased/revert-3c18ef0b.yml b/changelogs/unreleased/revert-3c18ef0b.yml
new file mode 100644
index 00000000000..b31edcd52ca
--- /dev/null
+++ b/changelogs/unreleased/revert-3c18ef0b.yml
@@ -0,0 +1,5 @@
+---
+title: Drop repository_read_only column from namespaces table
+merge_request: 52181
+author:
+type: changed
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-25-0.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-25-0.yml
new file mode 100644
index 00000000000..8db1aa7cdb0
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-25-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Runner Helm Chart to 0.25.0
+merge_request: 52095
+author:
+type: other
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index d5b3925131e..b01239d6aaf 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -270,6 +270,8 @@
- 1
- - project_update_repository_storage
- 1
+- - projects_git_garbage_collect
+ - 1
- - prometheus_create_default_alerts
- 1
- - propagate_integration
diff --git a/db/migrate/20210115084949_add_repository_read_only_to_groups.rb b/db/migrate/20210115084949_add_repository_read_only_to_groups.rb
deleted file mode 100644
index d25d3b90716..00000000000
--- a/db/migrate/20210115084949_add_repository_read_only_to_groups.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-class AddRepositoryReadOnlyToGroups < ActiveRecord::Migration[6.0]
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- def up
- with_lock_retries do
- add_column :namespaces, :repository_read_only, :boolean, default: false, null: false
- end
- end
-
- def down
- with_lock_retries do
- remove_column :namespaces, :repository_read_only
- end
- end
-end
diff --git a/db/migrate/20210121093618_remove_repository_read_only_to_groups.rb b/db/migrate/20210121093618_remove_repository_read_only_to_groups.rb
new file mode 100644
index 00000000000..a9607ca8fc6
--- /dev/null
+++ b/db/migrate/20210121093618_remove_repository_read_only_to_groups.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class RemoveRepositoryReadOnlyToGroups < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ if column_exists?(:namespaces, :repository_read_only)
+ with_lock_retries do
+ remove_column :namespaces, :repository_read_only # rubocop:disable Migration/RemoveColumn
+ end
+ end
+ end
+
+ def down
+ unless column_exists?(:namespaces, :repository_read_only)
+ with_lock_retries do
+ add_column :namespaces, :repository_read_only, :boolean, default: false, null: false
+ end
+ end
+ end
+end
diff --git a/db/schema_migrations/20210115084949 b/db/schema_migrations/20210115084949
deleted file mode 100644
index b2ccdfce89a..00000000000
--- a/db/schema_migrations/20210115084949
+++ /dev/null
@@ -1 +0,0 @@
-09766a70e36d4bbd8ecf7f5bba36c865873c1ac71f7eb8de04f70fd2c4da242e \ No newline at end of file
diff --git a/db/schema_migrations/20210121093618 b/db/schema_migrations/20210121093618
new file mode 100644
index 00000000000..f12566f1aa9
--- /dev/null
+++ b/db/schema_migrations/20210121093618
@@ -0,0 +1 @@
+e2be30f71b2a4a410b21e57ee53c3b54cf0214a08bc65cd92b2cb4b93bde9451 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 9c0e7e49208..36d16c3af3d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -14355,8 +14355,7 @@ CREATE TABLE namespaces (
shared_runners_enabled boolean DEFAULT true NOT NULL,
allow_descendants_override_disabled_shared_runners boolean DEFAULT false NOT NULL,
traversal_ids integer[] DEFAULT '{}'::integer[] NOT NULL,
- delayed_project_removal boolean DEFAULT false NOT NULL,
- repository_read_only boolean DEFAULT false NOT NULL
+ delayed_project_removal boolean DEFAULT false NOT NULL
);
CREATE SEQUENCE namespaces_id_seq
diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md
index 178fc438df2..8f369a05fbf 100644
--- a/doc/administration/housekeeping.md
+++ b/doc/administration/housekeeping.md
@@ -49,7 +49,7 @@ It is ultimately performed by the Gitaly RPC `FetchIntoObjectPool`.
This is the current call stack by which it is invoked:
1. `Repositories::HousekeepingService#execute_gitlab_shell_gc`
-1. `GitGarbageCollectWorker#perform`
+1. `Projects::GitGarbageCollectWorker#perform`
1. `Projects::GitDeduplicationService#fetch_from_source`
1. `ObjectPool#fetch`
1. `ObjectPoolService#fetch`
diff --git a/doc/api/boards.md b/doc/api/boards.md
index e86f0d846ed..021e4103228 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -8,8 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Every API call to boards must be authenticated.
-If a user is not a member of a project and the project is private, a `GET`
-request on that project will result to a `404` status code.
+If a user is not a member of a private project,
+a `GET` request on that project results in a `404` status code.
## List project issue boards
@@ -488,7 +488,7 @@ Example response:
## Delete a board list from a board
-Only for admins and project owners. Deletes the board list in question.
+Only for administrators and project owners. Deletes a board list.
```plaintext
DELETE /projects/:id/boards/:board_id/lists/:list_id
diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md
index cabab18ed71..0f04666893c 100644
--- a/doc/api/epic_links.md
+++ b/doc/api/epic_links.md
@@ -12,7 +12,8 @@ Manages parent-child [epic relationships](../user/group/epics/index.md#multi-lev
Every API call to `epic_links` must be authenticated.
-If a user makes a `GET` request to a private group they are not a member of, the result is a `404` status code.
+If a user is not a member of a private group, a `GET` request on that
+group results in a `404` status code.
Multi-level Epics are available only in GitLab [Ultimate/Gold](https://about.gitlab.com/pricing/).
If the Multi-level Epics feature is not available, a `403` status code is returned.
diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md
index 4c8630581f5..0bded97f189 100644
--- a/doc/user/project/issues/issue_data_and_actions.md
+++ b/doc/user/project/issues/issue_data_and_actions.md
@@ -291,7 +291,7 @@ supports [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-g
After you write a comment, you can:
-- Click **Comment** and to publish your comment.
+- Click **Comment** to publish your comment.
- Choose **Start thread** from the dropdown list and start a new [thread](../../discussions/index.md#threaded-discussions)
in that issue's main thread to discuss specific points. This invites other participants
to reply directly to your thread, keeping related comments grouped together.
diff --git a/lib/atlassian/jira_connect/client.rb b/lib/atlassian/jira_connect/client.rb
index 241f6e6cbc5..6f87b7b7d3c 100644
--- a/lib/atlassian/jira_connect/client.rb
+++ b/lib/atlassian/jira_connect/client.rb
@@ -4,7 +4,7 @@ module Atlassian
module JiraConnect
class Client < Gitlab::HTTP
def self.generate_update_sequence_id
- Gitlab::Metrics::System.monotonic_time.to_i
+ (Time.now.utc.to_f * 1000).round
end
def initialize(base_uri, shared_secret)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ffdb0ea9e4b..83f953004eb 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -31713,6 +31713,12 @@ msgstr ""
msgid "WebIDE|Merge request"
msgstr ""
+msgid "WebIDE|You need permission to edit files directly in this project."
+msgstr ""
+
+msgid "WebIDE|You need permission to edit files directly in this project. Fork this project to make your changes and submit a merge request."
+msgstr ""
+
msgid "Webhook"
msgstr ""
diff --git a/scripts/api/get_job_id.rb b/scripts/api/get_job_id.rb
index c7fe859db91..dd0b7fbada0 100755
--- a/scripts/api/get_job_id.rb
+++ b/scripts/api/get_job_id.rb
@@ -16,8 +16,8 @@ class JobFinder
def initialize(options)
@project = options.delete(:project)
- @pipeline_query = options.delete(:pipeline_query)
- @job_query = options.delete(:job_query)
+ @pipeline_query = options.delete(:pipeline_query) || DEFAULT_OPTIONS[:pipeline_query]
+ @job_query = options.delete(:job_query) || DEFAULT_OPTIONS[:job_query]
@pipeline_id = options.delete(:pipeline_id)
@job_name = options.delete(:job_name)
@artifact_path = options.delete(:artifact_path)
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index 17258f7042f..dec3d16f72f 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297400' do
+RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/299405' do
before do
project = create(:project, :repository)
sign_in project.owner
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index 5a39f2bcd98..9271dd9e2a0 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to add a .gitignore file' do
+RSpec.describe 'Projects > Files > User wants to add a .gitignore file', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/299405' do
before do
project = create(:project, :repository)
sign_in project.owner
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index 879cb6a65c8..2ad157ebd96 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
+RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/299405' do
before do
project = create(:project, :repository)
sign_in project.owner
diff --git a/spec/frontend/groups/members/mock_data.js b/spec/frontend/groups/members/mock_data.js
deleted file mode 100644
index b84c9c6d446..00000000000
--- a/spec/frontend/groups/members/mock_data.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export const membersJsonString =
- '[{"requested_at":null,"can_update":true,"can_remove":true,"can_override":false,"access_level":{"integer_value":50,"string_value":"Owner"},"source":{"id":323,"name":"My group / my subgroup","web_url":"http://127.0.0.1:3000/groups/my-group/my-subgroup"},"user":{"id":1,"name":"Administrator","username":"root","web_url":"http://127.0.0.1:3000/root","avatar_url":"https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80\u0026d=identicon","blocked":false,"two_factor_enabled":false},"id":524,"created_at":"2020-08-21T21:33:27.631Z","expires_at":null,"using_license":false,"group_sso":false,"group_managed_account":false}]';
-
-export const membersParsed = [
- {
- requestedAt: null,
- canUpdate: true,
- canRemove: true,
- canOverride: false,
- accessLevel: { integerValue: 50, stringValue: 'Owner' },
- source: {
- id: 323,
- name: 'My group / my subgroup',
- webUrl: 'http://127.0.0.1:3000/groups/my-group/my-subgroup',
- },
- user: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- avatarUrl:
- 'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon',
- blocked: false,
- twoFactorEnabled: false,
- },
- id: 524,
- createdAt: '2020-08-21T21:33:27.631Z',
- expiresAt: null,
- usingLicense: false,
- groupSso: false,
- groupManagedAccount: false,
- },
-];
diff --git a/spec/frontend/groups/members/utils_spec.js b/spec/frontend/groups/members/utils_spec.js
index 68945174e9d..0912e66e3e8 100644
--- a/spec/frontend/groups/members/utils_spec.js
+++ b/spec/frontend/groups/members/utils_spec.js
@@ -1,53 +1,14 @@
-import { membersJsonString, membersParsed } from './mock_data';
-import {
- parseDataAttributes,
- memberRequestFormatter,
- groupLinkRequestFormatter,
-} from '~/groups/members/utils';
+import { groupMemberRequestFormatter } from '~/groups/members/utils';
describe('group member utils', () => {
- describe('parseDataAttributes', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- el.setAttribute('data-members', membersJsonString);
- el.setAttribute('data-group-id', '234');
- el.setAttribute('data-can-manage-members', 'true');
- });
-
- afterEach(() => {
- el = null;
- });
-
- it('correctly parses the data attributes', () => {
- expect(parseDataAttributes(el)).toEqual({
- members: membersParsed,
- sourceId: 234,
- canManageMembers: true,
- });
- });
- });
-
- describe('memberRequestFormatter', () => {
+ describe('groupMemberRequestFormatter', () => {
it('returns expected format', () => {
expect(
- memberRequestFormatter({
+ groupMemberRequestFormatter({
accessLevel: 50,
expires_at: '2020-10-16',
}),
).toEqual({ group_member: { access_level: 50, expires_at: '2020-10-16' } });
});
});
-
- describe('groupLinkRequestFormatter', () => {
- it('returns expected format', () => {
- expect(
- groupLinkRequestFormatter({
- accessLevel: 50,
- expires_at: '2020-10-16',
- }),
- ).toEqual({ group_link: { group_access: 50, expires_at: '2020-10-16' } });
- });
- });
});
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index cec3fdfc05e..857d8f400a7 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -4,6 +4,7 @@ import { GlModal } from '@gitlab/ui';
import { projectData } from 'jest/ide/mock_data';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createStore } from '~/ide/stores';
import { COMMIT_TO_NEW_BRANCH } from '~/ide/stores/modules/commit/constants';
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
@@ -23,6 +24,9 @@ describe('IDE commit form', () => {
const createComponent = () => {
wrapper = shallowMount(CommitForm, {
store,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
stubs: {
GlModal: stubComponent(GlModal),
},
@@ -39,8 +43,21 @@ describe('IDE commit form', () => {
store.state.currentActivityView = leftSidebarViews.edit.name;
};
const findBeginCommitButton = () => wrapper.find('[data-testid="begin-commit-button"]');
+ const findBeginCommitButtonTooltip = () =>
+ wrapper.find('[data-testid="begin-commit-button-tooltip"]');
+ const findBeginCommitButtonData = () => ({
+ disabled: findBeginCommitButton().props('disabled'),
+ tooltip: getBinding(findBeginCommitButtonTooltip().element, 'gl-tooltip').value.title,
+ });
const findCommitButton = () => wrapper.find('[data-testid="commit-button"]');
+ const findCommitButtonTooltip = () => wrapper.find('[data-testid="commit-button-tooltip"]');
+ const findCommitButtonData = () => ({
+ disabled: findCommitButton().props('disabled'),
+ tooltip: getBinding(findCommitButtonTooltip().element, 'gl-tooltip').value.title,
+ });
+ const clickCommitButton = () => findCommitButton().vm.$emit('click');
const findForm = () => wrapper.find('form');
+ const submitForm = () => findForm().trigger('submit');
const findCommitMessageInput = () => wrapper.find(CommitMessageField);
const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val);
const findDiscardDraftButton = () => wrapper.find('[data-testid="discard-draft"]');
@@ -52,27 +69,40 @@ describe('IDE commit form', () => {
store.state.currentBranchId = 'master';
Vue.set(store.state.projects, 'abcproject', {
...projectData,
+ userPermissions: { pushCode: true },
});
});
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
+ // Notes:
+ // - When there are no changes, there is no commit button so there's nothing to test :)
describe.each`
- desc | stagedFiles | disabled
- ${'when there are changes'} | ${['test']} | ${false}
- ${'when there are no changes'} | ${[]} | ${true}
- `('$desc', ({ stagedFiles, disabled }) => {
+ desc | stagedFiles | userPermissions | viewFn | buttonFn | disabled | tooltip
+ ${'when there are no changes'} | ${[]} | ${{ pushCode: true }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${''}
+ ${'when there are changes'} | ${['test']} | ${{ pushCode: true }} | ${goToEditView} | ${findBeginCommitButtonData} | ${false} | ${''}
+ ${'when there are changes'} | ${['test']} | ${{ pushCode: true }} | ${goToCommitView} | ${findCommitButtonData} | ${false} | ${''}
+ ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${CommitForm.MSG_CANNOT_PUSH_CODE}
+ ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToCommitView} | ${findCommitButtonData} | ${true} | ${CommitForm.MSG_CANNOT_PUSH_CODE}
+ `('$desc', ({ stagedFiles, userPermissions, viewFn, buttonFn, disabled, tooltip }) => {
beforeEach(async () => {
store.state.stagedFiles = stagedFiles;
+ store.state.projects.abcproject.userPermissions = userPermissions;
createComponent();
});
- it(`begin button disabled=${disabled}`, async () => {
- expect(findBeginCommitButton().props('disabled')).toBe(disabled);
+ it(`at view=${viewFn.name}, ${buttonFn.name} has disabled=${disabled} tooltip=${tooltip}`, async () => {
+ viewFn();
+
+ await wrapper.vm.$nextTick();
+
+ expect(buttonFn()).toEqual({
+ disabled,
+ tooltip,
+ });
});
});
@@ -252,12 +282,21 @@ describe('IDE commit form', () => {
jest.spyOn(store, 'dispatch').mockResolvedValue();
});
- it('calls commitChanges', () => {
- findCommitButton().vm.$emit('click');
+ it.each([clickCommitButton, submitForm])('when %p, commits changes', (fn) => {
+ fn();
expect(store.dispatch).toHaveBeenCalledWith('commit/commitChanges', undefined);
});
+ it('when cannot push code, submitting does nothing', async () => {
+ store.state.projects.abcproject.userPermissions.pushCode = false;
+ await wrapper.vm.$nextTick();
+
+ submitForm();
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+
it.each`
createError | props
${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }}
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index 805fa898611..ef8c55c8b1c 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -1,9 +1,10 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { createStore } from '~/ide/stores';
import ErrorMessage from '~/ide/components/error_message.vue';
-import ide from '~/ide/components/ide.vue';
+import Ide from '~/ide/components/ide.vue';
import { file } from '../helpers';
import { projectData } from '../mock_data';
@@ -15,12 +16,12 @@ describe('WebIDE', () => {
let wrapper;
- function createComponent({ projData = emptyProjData, state = {} } = {}) {
+ const createComponent = ({ projData = emptyProjData, state = {} } = {}) => {
const store = createStore();
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
- store.state.projects.abcproject = { ...projData };
+ store.state.projects.abcproject = projData && { ...projData };
store.state.trees['abcproject/master'] = {
tree: [],
loading: false,
@@ -29,11 +30,13 @@ describe('WebIDE', () => {
store.state[key] = state[key];
});
- return shallowMount(ide, {
+ wrapper = shallowMount(Ide, {
store,
localVue,
});
- }
+ };
+
+ const findAlert = () => wrapper.find(GlAlert);
afterEach(() => {
wrapper.destroy();
@@ -42,7 +45,7 @@ describe('WebIDE', () => {
describe('ide component, empty repo', () => {
beforeEach(() => {
- wrapper = createComponent({
+ createComponent({
projData: {
empty_repo: true,
},
@@ -63,7 +66,7 @@ describe('WebIDE', () => {
`(
'should error message exists=$exists when errorMessage=$errorMessage',
async ({ errorMessage, exists }) => {
- wrapper = createComponent({
+ createComponent({
state: {
errorMessage,
},
@@ -78,12 +81,12 @@ describe('WebIDE', () => {
describe('onBeforeUnload', () => {
it('returns undefined when no staged files or changed files', () => {
- wrapper = createComponent();
+ createComponent();
expect(wrapper.vm.onBeforeUnload()).toBe(undefined);
});
it('returns warning text when their are changed files', () => {
- wrapper = createComponent({
+ createComponent({
state: {
changedFiles: [file()],
},
@@ -93,7 +96,7 @@ describe('WebIDE', () => {
});
it('returns warning text when their are staged files', () => {
- wrapper = createComponent({
+ createComponent({
state: {
stagedFiles: [file()],
},
@@ -104,7 +107,7 @@ describe('WebIDE', () => {
it('updates event object', () => {
const event = {};
- wrapper = createComponent({
+ createComponent({
state: {
stagedFiles: [file()],
},
@@ -118,7 +121,7 @@ describe('WebIDE', () => {
describe('non-existent branch', () => {
it('does not render "New file" button for non-existent branch when repo is not empty', () => {
- wrapper = createComponent({
+ createComponent({
state: {
projects: {},
},
@@ -130,7 +133,7 @@ describe('WebIDE', () => {
describe('branch with files', () => {
beforeEach(() => {
- wrapper = createComponent({
+ createComponent({
projData: {
empty_repo: false,
},
@@ -142,4 +145,31 @@ describe('WebIDE', () => {
});
});
});
+
+ it('when user cannot push code, shows alert', () => {
+ createComponent({
+ projData: {
+ userPermissions: {
+ pushCode: false,
+ },
+ },
+ });
+
+ expect(findAlert().props()).toMatchObject({
+ dismissible: false,
+ });
+ expect(findAlert().text()).toBe(Ide.MSG_CANNOT_PUSH_CODE);
+ });
+
+ it.each`
+ desc | projData
+ ${'when user can push code'} | ${{ userPermissions: { pushCode: true } }}
+ ${'when project is not ready'} | ${null}
+ `('$desc, no alert is shown', ({ projData }) => {
+ createComponent({
+ projData,
+ });
+
+ expect(findAlert().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index 1787f9e9361..1289c1aec75 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -2,6 +2,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import * as getters from '~/ide/stores/getters';
import { createStore } from '~/ide/stores';
import { file } from '../helpers';
+import { DEFAULT_PERMISSIONS } from '../../../../app/assets/javascripts/ide/constants';
const TEST_PROJECT_ID = 'test_project';
@@ -386,7 +387,9 @@ describe('IDE store getters', () => {
describe('findProjectPermissions', () => {
it('returns false if project not found', () => {
- expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual({});
+ expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual(
+ DEFAULT_PERMISSIONS,
+ );
});
it('finds permission in given project', () => {
diff --git a/spec/frontend/groups/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js
index 9847dacbec8..7b95e5ebd44 100644
--- a/spec/frontend/groups/members/components/app_spec.js
+++ b/spec/frontend/members/components/app_spec.js
@@ -2,13 +2,13 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import { GlAlert } from '@gitlab/ui';
-import App from '~/groups/members/components/app.vue';
+import MembersApp from '~/members/components/app.vue';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import * as commonUtils from '~/lib/utils/common_utils';
import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
import mutations from '~/members/store/mutations';
-describe('GroupMembersApp', () => {
+describe('MembersApp', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -25,7 +25,7 @@ describe('GroupMembersApp', () => {
mutations,
});
- wrapper = shallowMount(App, {
+ wrapper = shallowMount(MembersApp, {
localVue,
store,
...options,
diff --git a/spec/frontend/groups/members/index_spec.js b/spec/frontend/members/index_spec.js
index 5c717e53229..d73e3b03f07 100644
--- a/spec/frontend/groups/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -1,15 +1,15 @@
import { createWrapper } from '@vue/test-utils';
-import { initGroupMembersApp } from '~/groups/members';
-import GroupMembersApp from '~/groups/members/components/app.vue';
-import { membersJsonString, membersParsed } from './mock_data';
+import { initMembersApp } from '~/members/index';
+import MembersApp from '~/members/components/app.vue';
+import { membersJsonString, members } from './mock_data';
-describe('initGroupMembersApp', () => {
+describe('initMembersApp', () => {
let el;
let vm;
let wrapper;
const setup = () => {
- vm = initGroupMembersApp(el, {
+ vm = initMembersApp(el, {
tableFields: ['account'],
tableAttrs: { table: { 'data-qa-selector': 'members_list' } },
tableSortableFields: ['account'],
@@ -22,7 +22,7 @@ describe('initGroupMembersApp', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-members', membersJsonString);
- el.setAttribute('data-group-id', '234');
+ el.setAttribute('data-source-id', '234');
el.setAttribute('data-can-manage-members', 'true');
el.setAttribute('data-member-path', '/groups/foo-bar/-/group_members/:id');
@@ -36,10 +36,10 @@ describe('initGroupMembersApp', () => {
wrapper = null;
});
- it('renders `GroupMembersApp`', () => {
+ it('renders `MembersApp`', () => {
setup();
- expect(wrapper.find(GroupMembersApp).exists()).toBe(true);
+ expect(wrapper.find(MembersApp).exists()).toBe(true);
});
it('sets `currentUserId` in Vuex store', () => {
@@ -57,7 +57,7 @@ describe('initGroupMembersApp', () => {
});
});
- it('parses and sets `data-group-id` as `sourceId` in Vuex store', () => {
+ it('parses and sets `data-source-id` as `sourceId` in Vuex store', () => {
setup();
expect(vm.$store.state.sourceId).toBe(234);
@@ -72,7 +72,7 @@ describe('initGroupMembersApp', () => {
it('parses and sets `members` in Vuex store', () => {
setup();
- expect(vm.$store.state.members).toEqual(membersParsed);
+ expect(vm.$store.state.members).toEqual(members);
});
it('sets `tableFields` in Vuex store', () => {
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index e668f2a1998..5c00745a590 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -69,3 +69,5 @@ export const accessRequest = {
};
export const members = [member];
+
+export const membersJsonString = JSON.stringify(members);
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 7cd4e735b55..f123b2c6a91 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -9,9 +9,11 @@ import {
canOverride,
parseSortParam,
buildSortHref,
+ parseDataAttributes,
+ groupLinkRequestFormatter,
} from '~/members/utils';
import { DEFAULT_SORT } from '~/members/constants';
-import { member as memberMock, group, invite } from './mock_data';
+import { member as memberMock, group, invite, membersJsonString, members } from './mock_data';
const DIRECT_MEMBER_ID = 178;
const INHERITED_MEMBER_ID = 179;
@@ -229,4 +231,38 @@ describe('Members Utils', () => {
});
});
});
+
+ describe('parseDataAttributes', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.setAttribute('data-members', membersJsonString);
+ el.setAttribute('data-source-id', '234');
+ el.setAttribute('data-can-manage-members', 'true');
+ });
+
+ afterEach(() => {
+ el = null;
+ });
+
+ it('correctly parses the data attributes', () => {
+ expect(parseDataAttributes(el)).toEqual({
+ members,
+ sourceId: 234,
+ canManageMembers: true,
+ });
+ });
+ });
+
+ describe('groupLinkRequestFormatter', () => {
+ it('returns expected format', () => {
+ expect(
+ groupLinkRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ }),
+ ).toEqual({ group_link: { group_access: 50, expires_at: '2020-10-16' } });
+ });
+ });
});
diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb
index d75124b6da7..e83585e5efb 100644
--- a/spec/helpers/groups/group_members_helper_spec.rb
+++ b/spec/helpers/groups/group_members_helper_spec.rb
@@ -23,11 +23,11 @@ RSpec.describe Groups::GroupMembersHelper do
end
end
- describe '#linked_groups_data_json' do
+ describe '#group_group_links_data_json' do
include_context 'group_group_link'
it 'matches json schema' do
- json = helper.linked_groups_data_json(shared_group.shared_with_group_links)
+ json = helper.group_group_links_data_json(shared_group.shared_with_group_links)
expect(json).to match_schema('group_group_links')
end
@@ -81,13 +81,13 @@ RSpec.describe Groups::GroupMembersHelper do
expect(helper.group_members_list_data_attributes(group, present_members([group_member]))).to include({
members: helper.members_data_json(group, present_members([group_member])),
member_path: '/groups/foo-bar/-/group_members/:id',
- group_id: group.id,
+ source_id: group.id,
can_manage_members: 'true'
})
end
end
- describe '#linked_groups_list_data_attributes' do
+ describe '#group_group_links_list_data_attributes' do
include_context 'group_group_link'
before do
@@ -95,10 +95,10 @@ RSpec.describe Groups::GroupMembersHelper do
end
it 'returns expected hash' do
- expect(helper.linked_groups_list_data_attributes(shared_group)).to include({
- members: helper.linked_groups_data_json(shared_group.shared_with_group_links),
+ expect(helper.group_group_links_list_data_attributes(shared_group)).to include({
+ members: helper.group_group_links_data_json(shared_group.shared_with_group_links),
member_path: '/groups/foo-bar/-/group_links/:id',
- group_id: shared_group.id
+ source_id: shared_group.id
})
end
end
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index 2a4363ff2af..5c8d4282118 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -18,15 +18,15 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- around do |example|
- freeze_time { example.run }
- end
-
describe '.generate_update_sequence_id' do
- it 'returns monotonic_time converted it to integer' do
- allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(1.0)
+ it 'returns unix time in microseconds as integer', :aggregate_failures do
+ travel_to(Time.utc(1970, 1, 1, 0, 0, 1)) do
+ expect(described_class.generate_update_sequence_id).to eq(1000)
+ end
- expect(described_class.generate_update_sequence_id).to eq(1)
+ travel_to(Time.utc(1970, 1, 1, 0, 0, 5)) do
+ expect(described_class.generate_update_sequence_id).to eq(5000)
+ end
end
end
diff --git a/spec/services/projects/cleanup_service_spec.rb b/spec/services/projects/cleanup_service_spec.rb
index 6fd29813d98..f2c052d9397 100644
--- a/spec/services/projects/cleanup_service_spec.rb
+++ b/spec/services/projects/cleanup_service_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Projects::CleanupService do
end
it 'runs garbage collection on the repository' do
- expect_next_instance_of(GitGarbageCollectWorker) do |worker|
+ expect_next_instance_of(Projects::GitGarbageCollectWorker) do |worker|
expect(worker).to receive(:perform).with(project.id, :prune, "project_cleanup:gc:#{project.id}")
end
diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
index f201c7b1780..30f38b67732 100644
--- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
@@ -71,7 +71,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
it 'does not enqueue a GC run' do
expect { subject.execute }
- .not_to change(GitGarbageCollectWorker.jobs, :count)
+ .not_to change(Projects::GitGarbageCollectWorker.jobs, :count)
end
end
@@ -84,12 +84,12 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
stub_application_setting(housekeeping_enabled: false)
expect { subject.execute }
- .not_to change(GitGarbageCollectWorker.jobs, :count)
+ .not_to change(Projects::GitGarbageCollectWorker.jobs, :count)
end
it 'enqueues a GC run' do
expect { subject.execute }
- .to change(GitGarbageCollectWorker.jobs, :count).by(1)
+ .to change(Projects::GitGarbageCollectWorker.jobs, :count).by(1)
end
end
end
diff --git a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
index a174ae94b75..ad0dc389aeb 100644
--- a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
+++ b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
@@ -9,10 +9,10 @@ RSpec.shared_examples 'housekeeps repository' do
expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
expect(subject).to receive(:lease_key).and_return(:the_lease_key)
expect(subject).to receive(:task).and_return(:incremental_repack)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
+ expect(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
Sidekiq::Testing.fake! do
- expect { subject.execute }.to change(GitGarbageCollectWorker.jobs, :size).by(1)
+ expect { subject.execute }.to change(Projects::GitGarbageCollectWorker.jobs, :size).by(1)
end
end
@@ -38,7 +38,7 @@ RSpec.shared_examples 'housekeeps repository' do
end
it 'does not enqueue a job' do
- expect(GitGarbageCollectWorker).not_to receive(:perform_async)
+ expect(Projects::GitGarbageCollectWorker).not_to receive(:perform_async)
expect { subject.execute }.to raise_error(Repositories::HousekeepingService::LeaseTaken)
end
@@ -63,16 +63,16 @@ RSpec.shared_examples 'housekeeps repository' do
allow(subject).to receive(:lease_key).and_return(:the_lease_key)
# At push 200
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid)
+ expect(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid)
.once
# At push 50, 100, 150
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :full_repack, :the_lease_key, :the_uuid)
+ expect(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :full_repack, :the_lease_key, :the_uuid)
.exactly(3).times
# At push 10, 20, ... (except those above)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid)
+ expect(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid)
.exactly(16).times
# At push 6, 12, 18, ... (except those above)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid)
+ expect(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid)
.exactly(27).times
201.times do
@@ -90,7 +90,7 @@ RSpec.shared_examples 'housekeeps repository' do
allow(housekeeping).to receive(:try_obtain_lease).and_return(:gc_uuid)
allow(housekeeping).to receive(:lease_key).and_return(:gc_lease_key)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :gc_lease_key, :gc_uuid).twice
+ expect(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :gc_lease_key, :gc_uuid).twice
2.times do
housekeeping.execute
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 13089549086..3df64c35166 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -5,350 +5,22 @@ require 'fileutils'
require 'spec_helper'
RSpec.describe GitGarbageCollectWorker do
- include GitHelpers
-
let_it_be(:project) { create(:project, :repository) }
- let(:shell) { Gitlab::Shell.new }
- let!(:lease_uuid) { SecureRandom.uuid }
- let!(:lease_key) { "project_housekeeping:#{project.id}" }
- let(:params) { [project.id, task, lease_key, lease_uuid] }
-
- subject { described_class.new }
-
- shared_examples 'it calls Gitaly' do
- specify do
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(gitaly_task)
- .and_return(nil)
-
- subject.perform(*params)
- end
- end
- shared_examples 'it updates the project statistics' do
- 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
+ let(:lease_uuid) { SecureRandom.uuid }
+ let(:lease_key) { "project_housekeeping:#{project.id}" }
+ let(:task) { :full_repack }
+ let(:params) { [project.id, task, lease_key, lease_uuid] }
- 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
- end
+ subject { described_class.new }
describe "#perform" do
- let(:gitaly_task) { :garbage_collect }
- let(:task) { :gc }
-
- context 'with active lease_uuid' do
- before do
- allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
- end
-
- it_behaves_like 'it calls Gitaly'
- it_behaves_like 'it updates the project statistics'
-
- it "flushes ref caches when the task if 'gc'" do
- expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
- expect_any_instance_of(Repository).to receive(:expire_branches_cache).and_call_original
- expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
-
- subject.perform(*params)
- end
-
- it 'handles gRPC errors' do
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect).and_raise(GRPC::NotFound)
-
- expect { subject.perform(*params) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
- end
- end
-
- context 'with different lease than the active one' do
- before do
- allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid)
- end
-
- it 'returns silently' do
- expect_any_instance_of(Repository).not_to receive(:expire_branches_cache).and_call_original
- expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
-
- subject.perform(*params)
- end
- end
-
- context 'with no active lease' do
- let(:params) { [project.id] }
-
- before do
- allow(subject).to receive(:get_lease_uuid).and_return(false)
- end
-
- context 'when is able to get the lease' do
- before do
- allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
- end
-
- it_behaves_like 'it calls Gitaly'
- it_behaves_like 'it updates the project statistics'
-
- it "flushes ref caches when the task if 'gc'" do
- expect(subject).to receive(:get_lease_uuid).with("git_gc:#{task}:#{project.id}").and_return(false)
- expect_any_instance_of(Repository).to receive(:expire_branches_cache).and_call_original
- expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
-
- subject.perform(*params)
- end
-
- context 'when the repository has joined a pool' do
- let!(:pool) { create(:pool_repository, :ready) }
- let(:project) { pool.source_project }
-
- it 'ensures the repositories are linked' do
- expect_any_instance_of(PoolRepository).to receive(:link_repository).once
-
- subject.perform(*params)
- end
- end
-
- context 'LFS object garbage collection' do
- before do
- stub_lfs_setting(enabled: true)
- end
-
- let_it_be(:lfs_reference) { create(:lfs_objects_project, project: project) }
- let(:lfs_object) { lfs_reference.lfs_object }
-
- it 'cleans up unreferenced LFS objects' do
- expect_next_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences) do |svc|
- expect(svc.project).to eq(project)
- expect(svc.dry_run).to be_falsy
- expect(svc).to receive(:run!).and_call_original
- end
-
- subject.perform(*params)
-
- expect(project.lfs_objects.reload).not_to include(lfs_object)
- end
-
- it 'catches and logs exceptions' do
- expect_any_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences)
- .to receive(:run!)
- .and_raise(/Failed/)
-
- expect(Gitlab::GitLogger).to receive(:warn)
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
-
- 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(Gitlab::Cleanup::OrphanLfsFileReferences).not_to receive(:run!)
-
- subject.perform(*params)
-
- expect(project.lfs_objects.reload).to include(lfs_object)
- end
- end
- end
-
- context 'when no lease can be obtained' do
- before do
- expect(subject).to receive(:try_obtain_lease).and_return(false)
- end
-
- it 'returns silently' do
- expect(subject).not_to receive(:command)
- expect_any_instance_of(Repository).not_to receive(:expire_branches_cache).and_call_original
- expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
-
- subject.perform(*params)
- end
- end
- end
-
- context "repack_full" do
- let(:task) { :full_repack }
- let(:gitaly_task) { :repack_full }
-
- before do
- expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ it 'calls the Projects::GitGarbageGitGarbageCollectWorker with the same params' do
+ expect_next_instance_of(Projects::GitGarbageCollectWorker) do |instance|
+ expect(instance).to receive(:perform).with(*params)
end
- it_behaves_like 'it calls Gitaly'
- it_behaves_like 'it updates the project statistics'
- end
-
- context "pack_refs" do
- let(:task) { :pack_refs }
- let(:gitaly_task) { :pack_refs }
-
- before do
- expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
- end
-
- it "calls Gitaly" do
- expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(task)
- .and_return(nil)
-
- subject.perform(*params)
- end
-
- it 'does not update the project statistics' do
- expect(Projects::UpdateStatisticsService).not_to receive(:new)
-
- subject.perform(*params)
- end
- end
-
- context "repack_incremental" do
- let(:task) { :incremental_repack }
- let(:gitaly_task) { :repack_incremental }
-
- before do
- expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
- end
-
- it_behaves_like 'it calls Gitaly'
- it_behaves_like 'it updates the project statistics'
- end
-
- shared_examples 'gc tasks' do
- before do
- allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
- allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled)
- end
-
- it 'incremental repack adds a new packfile' do
- create_objects(project)
- before_packs = packs(project)
-
- expect(before_packs.count).to be >= 1
-
- subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid)
- after_packs = packs(project)
-
- # Exactly one new pack should have been created
- expect(after_packs.count).to eq(before_packs.count + 1)
-
- # Previously existing packs are still around
- expect(before_packs & after_packs).to eq(before_packs)
- end
-
- it 'full repack consolidates into 1 packfile' do
- create_objects(project)
- subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid)
- before_packs = packs(project)
-
- expect(before_packs.count).to be >= 2
-
- subject.perform(project.id, 'full_repack', lease_key, lease_uuid)
- after_packs = packs(project)
-
- expect(after_packs.count).to eq(1)
-
- # Previously existing packs should be gone now
- expect(after_packs - before_packs).to eq(after_packs)
-
- expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
- end
-
- it 'gc consolidates into 1 packfile and updates packed-refs' do
- create_objects(project)
- before_packs = packs(project)
- before_packed_refs = packed_refs(project)
-
- expect(before_packs.count).to be >= 1
-
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService)
- .to receive(:garbage_collect)
- .with(bitmaps_enabled, prune: false)
- .and_call_original
-
- subject.perform(project.id, 'gc', lease_key, lease_uuid)
- after_packed_refs = packed_refs(project)
- after_packs = packs(project)
-
- expect(after_packs.count).to eq(1)
-
- # Previously existing packs should be gone now
- expect(after_packs - before_packs).to eq(after_packs)
-
- # The packed-refs file should have been updated during 'git gc'
- expect(before_packed_refs).not_to eq(after_packed_refs)
-
- expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
- end
-
- it 'cleans up repository after finishing' do
- expect_any_instance_of(Project).to receive(:cleanup).and_call_original
-
- subject.perform(project.id, 'gc', lease_key, lease_uuid)
- end
-
- it 'prune calls garbage_collect with the option prune: true' do
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService)
- .to receive(:garbage_collect)
- .with(bitmaps_enabled, prune: true)
- .and_return(nil)
-
- subject.perform(project.id, 'prune', lease_key, lease_uuid)
- end
- end
-
- context 'with bitmaps enabled' do
- let(:bitmaps_enabled) { true }
-
- include_examples 'gc tasks'
- end
-
- context 'with bitmaps disabled' do
- let(:bitmaps_enabled) { false }
-
- include_examples 'gc tasks'
- end
- end
-
- # Create a new commit on a random new branch
- def create_objects(project)
- rugged = rugged_repo(project.repository)
- old_commit = rugged.branches.first.target
- new_commit_sha = Rugged::Commit.create(
- rugged,
- message: "hello world #{SecureRandom.hex(6)}",
- author: { email: 'foo@bar', name: 'baz' },
- committer: { email: 'foo@bar', name: 'baz' },
- tree: old_commit.tree,
- parents: [old_commit]
- )
- rugged.references.create("refs/heads/#{SecureRandom.hex(6)}", new_commit_sha)
- end
-
- def packs(project)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- Dir["#{project.repository.path_to_repo}/objects/pack/*.pack"]
+ subject.perform(*params)
end
end
-
- def packed_refs(project)
- path = "#{project.repository.path_to_repo}/packed-refs"
- FileUtils.touch(path)
- File.read(path)
- end
-
- def bitmap_path(pack)
- pack.sub(/\.pack\z/, '.bitmap')
- end
end
diff --git a/spec/workers/projects/git_garbage_collect_worker_spec.rb b/spec/workers/projects/git_garbage_collect_worker_spec.rb
new file mode 100644
index 00000000000..9ef1cce117e
--- /dev/null
+++ b/spec/workers/projects/git_garbage_collect_worker_spec.rb
@@ -0,0 +1,374 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+require 'spec_helper'
+
+RSpec.describe Projects::GitGarbageCollectWorker do
+ include GitHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+
+ let!(:lease_uuid) { SecureRandom.uuid }
+ let!(:lease_key) { "project_housekeeping:#{project.id}" }
+ let(:params) { [project.id, task, lease_key, lease_uuid] }
+ let(:shell) { Gitlab::Shell.new }
+ let(:repository) { project.repository }
+
+ subject { described_class.new }
+
+ before do
+ allow(subject).to receive(:find_project).and_return(project)
+ end
+
+ shared_examples 'it calls Gitaly' do
+ specify do
+ repository_service = instance_double(Gitlab::GitalyClient::RepositoryService)
+
+ expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service)
+ expect(repository_service).to receive(gitaly_task)
+
+ subject.perform(*params)
+ end
+ end
+
+ shared_examples 'it updates the project statistics' do
+ 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)
+ end
+
+ subject.perform(*params)
+ end
+
+ it 'does nothing if the database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect(Projects::UpdateStatisticsService).not_to receive(:new)
+
+ subject.perform(*params)
+ end
+ end
+
+ describe '#perform', :aggregate_failures do
+ let(:gitaly_task) { :garbage_collect }
+ let(:task) { :gc }
+
+ context 'with active lease_uuid' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the project statistics'
+
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
+ expect(repository).to receive(:expire_branches_cache).and_call_original
+ expect(repository).to receive(:branch_names).and_call_original
+ expect(repository).to receive(:has_visible_content?).and_call_original
+ expect(repository.raw_repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+
+ it 'handles gRPC errors' do
+ allow_next_instance_of(Gitlab::GitalyClient::RepositoryService, repository.raw_repository) do |instance|
+ allow(instance).to receive(:garbage_collect).and_raise(GRPC::NotFound)
+ end
+
+ expect { subject.perform(*params) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
+ end
+ end
+
+ context 'with different lease than the active one' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid)
+ end
+
+ it 'returns silently' do
+ expect(repository).not_to receive(:expire_branches_cache).and_call_original
+ expect(repository).not_to receive(:branch_names).and_call_original
+ expect(repository).not_to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+ end
+
+ context 'with no active lease' do
+ let(:params) { [project.id] }
+
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(false)
+ end
+
+ context 'when is able to get the lease' do
+ before do
+ allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the project statistics'
+
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:get_lease_uuid).with("git_gc:#{task}:#{project.id}").and_return(false)
+ expect(repository).to receive(:expire_branches_cache).and_call_original
+ expect(repository).to receive(:branch_names).and_call_original
+ expect(repository).to receive(:has_visible_content?).and_call_original
+ expect(repository.raw_repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+
+ context 'when the repository has joined a pool' do
+ let!(:pool) { create(:pool_repository, :ready) }
+ let(:project) { pool.source_project }
+
+ it 'ensures the repositories are linked' do
+ expect(project.pool_repository).to receive(:link_repository).once
+
+ subject.perform(*params)
+ end
+ end
+
+ context 'LFS object garbage collection' do
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ let_it_be(:lfs_reference) { create(:lfs_objects_project, project: project) }
+ let(:lfs_object) { lfs_reference.lfs_object }
+
+ it 'cleans up unreferenced LFS objects' do
+ expect_next_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences) do |svc|
+ expect(svc.project).to eq(project)
+ expect(svc.dry_run).to be_falsy
+ expect(svc).to receive(:run!).and_call_original
+ end
+
+ subject.perform(*params)
+
+ expect(project.lfs_objects.reload).not_to include(lfs_object)
+ end
+
+ it 'catches and logs exceptions' do
+ allow_next_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences) do |svc|
+ allow(svg).to receive(:run!).and_raise(/Failed/)
+ end
+
+ expect(Gitlab::GitLogger).to receive(:warn)
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+
+ subject.perform(*params)
+ end
+
+ it 'does nothing if the database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ expect(Gitlab::Cleanup::OrphanLfsFileReferences).not_to receive(:new)
+
+ subject.perform(*params)
+
+ expect(project.lfs_objects.reload).to include(lfs_object)
+ end
+ end
+ end
+
+ context 'when no lease can be obtained' do
+ it 'returns silently' do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+
+ expect(subject).not_to receive(:command)
+ expect(repository).not_to receive(:expire_branches_cache).and_call_original
+ expect(repository).not_to receive(:branch_names).and_call_original
+ expect(repository).not_to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+ end
+ end
+
+ context 'repack_full' do
+ let(:task) { :full_repack }
+ let(:gitaly_task) { :repack_full }
+
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the project statistics'
+ end
+
+ context 'pack_refs' do
+ let(:task) { :pack_refs }
+ let(:gitaly_task) { :pack_refs }
+
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it 'calls Gitaly' do
+ repository_service = instance_double(Gitlab::GitalyClient::RefService)
+
+ expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service)
+ expect(repository_service).to receive(gitaly_task)
+
+ subject.perform(*params)
+ end
+
+ it 'does not update the project statistics' do
+ expect(Projects::UpdateStatisticsService).not_to receive(:new)
+
+ subject.perform(*params)
+ end
+ end
+
+ context 'repack_incremental' do
+ let(:task) { :incremental_repack }
+ let(:gitaly_task) { :repack_incremental }
+
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the project statistics'
+ end
+
+ shared_examples 'gc tasks' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled)
+ end
+
+ it 'incremental repack adds a new packfile' do
+ create_objects(project)
+ before_packs = packs(project)
+
+ expect(before_packs.count).to be >= 1
+
+ subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid)
+ after_packs = packs(project)
+
+ # Exactly one new pack should have been created
+ expect(after_packs.count).to eq(before_packs.count + 1)
+
+ # Previously existing packs are still around
+ expect(before_packs & after_packs).to eq(before_packs)
+ end
+
+ it 'full repack consolidates into 1 packfile' do
+ create_objects(project)
+ subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid)
+ before_packs = packs(project)
+
+ expect(before_packs.count).to be >= 2
+
+ subject.perform(project.id, 'full_repack', lease_key, lease_uuid)
+ after_packs = packs(project)
+
+ expect(after_packs.count).to eq(1)
+
+ # Previously existing packs should be gone now
+ expect(after_packs - before_packs).to eq(after_packs)
+
+ expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
+ end
+
+ it 'gc consolidates into 1 packfile and updates packed-refs' do
+ create_objects(project)
+ before_packs = packs(project)
+ before_packed_refs = packed_refs(project)
+
+ expect(before_packs.count).to be >= 1
+
+ # It's quite difficult to use `expect_next_instance_of` in this place
+ # because the RepositoryService is instantiated several times to do
+ # some repository calls like `exists?`, `create_repository`, ... .
+ # Therefore, since we're instantiating the object several times,
+ # RSpec has troubles figuring out which instance is the next and which
+ # one we want to mock.
+ # Besides, at this point, we actually want to perform the call to Gitaly,
+ # otherwise we would just use `instance_double` like in other parts of the
+ # spec file.
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService) # rubocop:disable RSpec/AnyInstanceOf
+ .to receive(:garbage_collect)
+ .with(bitmaps_enabled, prune: false)
+ .and_call_original
+
+ subject.perform(project.id, 'gc', lease_key, lease_uuid)
+ after_packed_refs = packed_refs(project)
+ after_packs = packs(project)
+
+ expect(after_packs.count).to eq(1)
+
+ # Previously existing packs should be gone now
+ expect(after_packs - before_packs).to eq(after_packs)
+
+ # The packed-refs file should have been updated during 'git gc'
+ expect(before_packed_refs).not_to eq(after_packed_refs)
+
+ expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
+ end
+
+ it 'cleans up repository after finishing' do
+ expect(project).to receive(:cleanup).and_call_original
+
+ subject.perform(project.id, 'gc', lease_key, lease_uuid)
+ end
+
+ it 'prune calls garbage_collect with the option prune: true' do
+ repository_service = instance_double(Gitlab::GitalyClient::RepositoryService)
+
+ expect(subject).to receive(:get_gitaly_client).with(:prune, repository.raw_repository).and_return(repository_service)
+ expect(repository_service).to receive(:garbage_collect).with(bitmaps_enabled, prune: true)
+
+ subject.perform(project.id, 'prune', lease_key, lease_uuid)
+ end
+ end
+
+ context 'with bitmaps enabled' do
+ let(:bitmaps_enabled) { true }
+
+ include_examples 'gc tasks'
+ end
+
+ context 'with bitmaps disabled' do
+ let(:bitmaps_enabled) { false }
+
+ include_examples 'gc tasks'
+ end
+ end
+
+ # Create a new commit on a random new branch
+ def create_objects(project)
+ rugged = rugged_repo(project.repository)
+ old_commit = rugged.branches.first.target
+ new_commit_sha = Rugged::Commit.create(
+ rugged,
+ message: "hello world #{SecureRandom.hex(6)}",
+ author: { email: 'foo@bar', name: 'baz' },
+ committer: { email: 'foo@bar', name: 'baz' },
+ tree: old_commit.tree,
+ parents: [old_commit]
+ )
+ rugged.references.create("refs/heads/#{SecureRandom.hex(6)}", new_commit_sha)
+ end
+
+ def packs(project)
+ Dir["#{path_to_repo}/objects/pack/*.pack"]
+ end
+
+ def packed_refs(project)
+ path = File.join(path_to_repo, 'packed-refs')
+ FileUtils.touch(path)
+ File.read(path)
+ end
+
+ def path_to_repo
+ @path_to_repo ||= File.join(TestEnv.repos_path, project.repository.relative_path)
+ end
+
+ def bitmap_path(pack)
+ pack.sub(/\.pack\z/, '.bitmap')
+ end
+end