Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
10cc2d7a72
commit
899bb5c4a9
|
|
@ -1,7 +1,11 @@
|
|||
import axios from 'axios';
|
||||
|
||||
const getJwt = async () => {
|
||||
return AP.context.getToken();
|
||||
export const getJwt = () => {
|
||||
return new Promise((resolve) => {
|
||||
AP.context.getToken((token) => {
|
||||
resolve(token);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const addSubscription = async (addPath, namespace) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlTabs, GlTab, GlLoadingIcon, GlPagination } from '@gitlab/ui';
|
||||
import { GlTabs, GlTab, GlLoadingIcon, GlPagination, GlAlert } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
|
||||
import { fetchGroups } from '~/jira_connect/api';
|
||||
|
|
@ -12,6 +12,7 @@ export default {
|
|||
GlTab,
|
||||
GlLoadingIcon,
|
||||
GlPagination,
|
||||
GlAlert,
|
||||
GroupsListItem,
|
||||
},
|
||||
inject: {
|
||||
|
|
@ -26,6 +27,7 @@ export default {
|
|||
page: 1,
|
||||
perPage: defaultPerPage,
|
||||
totalItems: 0,
|
||||
errorMessage: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -46,8 +48,7 @@ export default {
|
|||
this.groups = response.data;
|
||||
})
|
||||
.catch(() => {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(s__('Integrations|Failed to load namespaces. Please try again.'));
|
||||
this.errorMessage = s__('Integrations|Failed to load namespaces. Please try again.');
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
|
|
@ -58,31 +59,42 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-tabs>
|
||||
<gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
|
||||
<gl-loading-icon v-if="isLoading" size="md" />
|
||||
<div v-else-if="groups.length === 0" class="gl-text-center">
|
||||
<h5>{{ s__('Integrations|No available namespaces.') }}</h5>
|
||||
<p class="gl-mt-5">
|
||||
{{
|
||||
s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<ul v-else class="gl-list-style-none gl-pl-0">
|
||||
<groups-list-item v-for="group in groups" :key="group.id" :group="group" />
|
||||
</ul>
|
||||
<div>
|
||||
<gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" @dismiss="errorMessage = null">
|
||||
{{ errorMessage }}
|
||||
</gl-alert>
|
||||
|
||||
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
|
||||
<gl-pagination
|
||||
v-if="totalItems > perPage && groups.length > 0"
|
||||
v-model="page"
|
||||
class="gl-mb-0"
|
||||
:per-page="perPage"
|
||||
:total-items="totalItems"
|
||||
@input="loadGroups"
|
||||
/>
|
||||
</div>
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
<gl-tabs>
|
||||
<gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
|
||||
<gl-loading-icon v-if="isLoading" size="md" />
|
||||
<div v-else-if="groups.length === 0" class="gl-text-center">
|
||||
<h5>{{ s__('Integrations|No available namespaces.') }}</h5>
|
||||
<p class="gl-mt-5">
|
||||
{{
|
||||
s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<ul v-else class="gl-list-style-none gl-pl-0">
|
||||
<groups-list-item
|
||||
v-for="group in groups"
|
||||
:key="group.id"
|
||||
:group="group"
|
||||
@error="errorMessage = $event"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
|
||||
<gl-pagination
|
||||
v-if="totalItems > perPage && groups.length > 0"
|
||||
v-model="page"
|
||||
class="gl-mb-0"
|
||||
:per-page="perPage"
|
||||
:total-items="totalItems"
|
||||
@input="loadGroups"
|
||||
/>
|
||||
</div>
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
<script>
|
||||
import { GlIcon, GlAvatar } from '@gitlab/ui';
|
||||
import { GlAvatar, GlButton, GlIcon } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
import { addSubscription } from '~/jira_connect/api';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlAvatar,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
},
|
||||
inject: {
|
||||
subscriptionsPath: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
props: {
|
||||
group: {
|
||||
|
|
@ -12,6 +21,31 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.isLoading = true;
|
||||
|
||||
addSubscription(this.subscriptionsPath, this.group.full_path)
|
||||
.then(() => {
|
||||
AP.navigator.reload();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$emit(
|
||||
'error',
|
||||
error?.response?.data?.error ||
|
||||
s__('Integrations|Failed to link namespace. Please try again.'),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -36,6 +70,14 @@ export default {
|
|||
<p class="gl-mt-2! gl-mb-0 gl-text-gray-600" v-text="group.description"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gl-button
|
||||
category="secondary"
|
||||
variant="success"
|
||||
:loading="isLoading"
|
||||
@click.prevent="onClick"
|
||||
>{{ __('Link') }}</gl-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import $ from 'jquery';
|
||||
import setConfigs from '@gitlab/ui/dist/config';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
|
|
@ -10,8 +9,6 @@ import { addSubscription, removeSubscription } from '~/jira_connect/api';
|
|||
import createStore from './store';
|
||||
import { SET_ERROR_MESSAGE } from './store/mutation_types';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const store = createStore();
|
||||
|
||||
/**
|
||||
|
|
@ -73,13 +70,14 @@ function initJiraConnect() {
|
|||
Vue.use(Translate);
|
||||
Vue.use(GlFeatureFlagsPlugin);
|
||||
|
||||
const { groupsPath } = el.dataset;
|
||||
const { groupsPath, subscriptionsPath } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
provide: {
|
||||
groupsPath,
|
||||
subscriptionsPath,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(JiraConnectApp);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import mutations from './mutations';
|
||||
import state from './state';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default () =>
|
||||
new Vuex.Store({
|
||||
state,
|
||||
mutations,
|
||||
state,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export default {
|
|||
<gl-button
|
||||
v-if="hasSidebarButton"
|
||||
class="d-sm-none js-sidebar-build-toggle gl-ml-auto"
|
||||
icon="angle-double-left"
|
||||
icon="chevron-double-lg-left"
|
||||
:aria-label="__('Toggle sidebar')"
|
||||
@click="onClickSidebarButton"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ pre {
|
|||
}
|
||||
|
||||
hr {
|
||||
margin: 24px 0;
|
||||
margin: 1.5rem 0;
|
||||
border-top: 1px solid $gray-darker;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,14 @@ module JiraConnectHelper
|
|||
Feature.enabled?(:new_jira_connect_ui, type: :development, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
def jira_connect_app_data
|
||||
def jira_connect_app_data(subscriptions)
|
||||
return {} unless new_jira_connect_ui?
|
||||
|
||||
skip_groups = subscriptions.map(&:namespace_id)
|
||||
|
||||
{
|
||||
groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER })
|
||||
groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER, skip_groups: skip_groups }),
|
||||
subscriptions_path: jira_connect_subscriptions_path
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -55,15 +55,20 @@ class AuditEvent < ApplicationRecord
|
|||
end
|
||||
|
||||
def author_name
|
||||
lazy_author.name
|
||||
author&.name
|
||||
end
|
||||
|
||||
def formatted_details
|
||||
details.merge(details.slice(:from, :to).transform_values(&:to_s))
|
||||
end
|
||||
|
||||
def author
|
||||
lazy_author&.itself.presence ||
|
||||
::Gitlab::Audit::NullAuthor.for(author_id, (self[:author_name] || details[:author_name]))
|
||||
end
|
||||
|
||||
def lazy_author
|
||||
BatchLoader.for(author_id).batch(default_value: default_author_value, replace_methods: false) do |author_ids, loader|
|
||||
BatchLoader.for(author_id).batch(replace_methods: false) do |author_ids, loader|
|
||||
User.select(:id, :name, :username).where(id: author_ids).find_each do |user|
|
||||
loader.call(user.id, user)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ module OptimizedIssuableLabelFilter
|
|||
def by_label(items)
|
||||
return items unless params.labels?
|
||||
|
||||
return super if Feature.disabled?(:optimized_issuable_label_filter)
|
||||
return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
|
||||
|
||||
target_model = items.model
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ module OptimizedIssuableLabelFilter
|
|||
# Taken from IssuableFinder
|
||||
def count_by_state
|
||||
return super if root_namespace.nil?
|
||||
return super if Feature.disabled?(:optimized_issuable_label_filter)
|
||||
return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
|
||||
|
||||
count_params = params.merge(state: nil, sort: nil, force_cte: true)
|
||||
finder = self.class.new(current_user, count_params)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ module Repositories
|
|||
Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
|
||||
end
|
||||
|
||||
def git_garbage_collect_worker_klass
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pushes_since_gc_redis_shared_state_key
|
||||
|
|
|
|||
|
|
@ -2522,6 +2522,11 @@ class Project < ApplicationRecord
|
|||
tracing_setting&.external_url
|
||||
end
|
||||
|
||||
override :git_garbage_collect_worker_klass
|
||||
def git_garbage_collect_worker_klass
|
||||
Projects::GitGarbageCollectWorker
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_service(services, name)
|
||||
|
|
|
|||
|
|
@ -256,6 +256,15 @@ class Wiki
|
|||
def after_post_receive
|
||||
end
|
||||
|
||||
override :git_garbage_collect_worker_klass
|
||||
def git_garbage_collect_worker_klass
|
||||
Wikis::GitGarbageCollectWorker
|
||||
end
|
||||
|
||||
def cleanup
|
||||
@repository = nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def commit_details(action, message = nil, title = nil)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ module Repositories
|
|||
private
|
||||
|
||||
def execute_gitlab_shell_gc(lease_uuid)
|
||||
Projects::GitGarbageCollectWorker.perform_async(@resource.id, task, lease_key, lease_uuid)
|
||||
@resource.git_garbage_collect_worker_klass.perform_async(@resource.id, task, lease_key, lease_uuid)
|
||||
ensure
|
||||
if pushes_since_gc >= gc_period
|
||||
Gitlab::Metrics.measure(:reset_pushes_since_gc) do
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ module Suggestions
|
|||
|
||||
Suggestion.id_in(suggestion_set.suggestions)
|
||||
.update_all(commit_id: result[:result], applied: true)
|
||||
|
||||
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
|
||||
.track_apply_suggestion_action(user: current_user)
|
||||
end
|
||||
|
||||
def multi_service
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ module Suggestions
|
|||
rows.in_groups_of(100, false) do |rows|
|
||||
Gitlab::Database.bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert
|
||||
end
|
||||
|
||||
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter.track_add_suggestion_action(user: @note.author)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
.gl-mt-5
|
||||
%p Note: this integration only works with accounts on GitLab.com (SaaS).
|
||||
- else
|
||||
.js-jira-connect-app{ data: jira_connect_app_data }
|
||||
.js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
|
||||
|
||||
- unless new_jira_connect_ui?
|
||||
%form#add-subscription-form.subscription-form{ action: jira_connect_subscriptions_path }
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
Link namespace to Jira
|
||||
|
||||
- if @subscriptions.present?
|
||||
%table.subscriptions
|
||||
%table.subscriptions.gl-w-full
|
||||
%thead
|
||||
%tr
|
||||
%th Namespace
|
||||
|
|
|
|||
|
|
@ -27,6 +27,4 @@
|
|||
= s_('Profiles|Created%{time_ago}'.html_safe) % { time_ago: time_ago_with_tooltip(key.created_at, html_class: 'gl-ml-2')}
|
||||
- if key.can_delete?
|
||||
.gl-ml-3
|
||||
= button_to '#', class: "btn btn-default gl-button btn-default-tertiary js-confirm-modal-button", data: ssh_key_delete_modal_data(key, path_to_key(key, is_admin)) do
|
||||
%span.sr-only= _('Delete')
|
||||
= sprite_icon('remove')
|
||||
= render 'shared/ssh_keys/key_delete', html_class: "btn btn-default gl-button btn-default-tertiary js-confirm-modal-button", button_data: ssh_key_delete_modal_data(key, path_to_key(key, is_admin))
|
||||
|
|
|
|||
|
|
@ -38,4 +38,4 @@
|
|||
.col-md-12
|
||||
.float-right
|
||||
- if @key.can_delete?
|
||||
= button_to _('Delete'), '#', class: "btn btn-danger gl-button delete-key js-confirm-modal-button", data: ssh_key_delete_modal_data(@key, path_to_key(@key, is_admin))
|
||||
= render 'shared/ssh_keys/key_delete', text: _('Delete'), html_class: "btn btn-danger gl-button delete-key js-confirm-modal-button", button_data: ssh_key_delete_modal_data(@key, path_to_key(@key, is_admin))
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@
|
|||
%br
|
||||
%br
|
||||
- if @project.group_runners_enabled?
|
||||
= link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do
|
||||
= link_to toggle_group_runners_project_runners_path(@project), class: 'btn gl-button btn-warning-secondary', method: :post do
|
||||
= _('Disable group runners')
|
||||
- else
|
||||
= link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success btn-inverted', method: :post do
|
||||
= link_to toggle_group_runners_project_runners_path(@project), class: 'btn gl-button btn-success btn-inverted', method: :post do
|
||||
= _('Enable group runners')
|
||||
|
||||
= _('for this project')
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
= sprite_icon('lock')
|
||||
|
||||
%small.edit-runner
|
||||
= link_to edit_project_runner_path(@project, runner), class: 'btn btn-edit' do
|
||||
= sprite_icon('pencil')
|
||||
= link_to edit_project_runner_path(@project, runner), class: 'btn gl-button btn-edit' do
|
||||
= sprite_icon('pencil', css_class: 'gl-my-2')
|
||||
- else
|
||||
%span.commit-sha
|
||||
= runner.short_sha
|
||||
|
|
@ -19,18 +19,18 @@
|
|||
.float-right
|
||||
- if @project_runners.include?(runner)
|
||||
- if runner.active?
|
||||
= link_to _('Pause'), pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
|
||||
= link_to _('Pause'), pause_project_runner_path(@project, runner), method: :post, class: 'btn gl-button btn-sm btn-danger', data: { confirm: _("Are you sure?") }
|
||||
- else
|
||||
= link_to _('Resume'), resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm'
|
||||
= link_to _('Resume'), resume_project_runner_path(@project, runner), method: :post, class: 'btn gl-button btn-success btn-sm'
|
||||
- if runner.belongs_to_one_project?
|
||||
= link_to _('Remove runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
|
||||
= link_to _('Remove runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn gl-button btn-danger btn-sm'
|
||||
- else
|
||||
- runner_project = @project.runner_projects.find_by(runner_id: runner) # rubocop: disable CodeReuse/ActiveRecord
|
||||
= link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
|
||||
= link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn gl-button btn-danger btn-sm'
|
||||
- elsif runner.project_type?
|
||||
= form_for [@project, @project.runner_projects.new] do |f|
|
||||
= f.hidden_field :runner_id, value: runner.id
|
||||
= f.submit _('Enable for this project'), class: 'btn btn-sm'
|
||||
= f.submit _('Enable for this project'), class: 'btn gl-button btn-sm'
|
||||
.float-right
|
||||
%small.light
|
||||
\##{runner.id}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
= _('Shared runners disabled on group level')
|
||||
- else
|
||||
- if @project.shared_runners_enabled?
|
||||
= link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do
|
||||
= link_to toggle_shared_runners_project_runners_path(@project), class: 'btn gl-button btn-warning-secondary', method: :post do
|
||||
= _('Disable shared runners')
|
||||
- else
|
||||
= link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do
|
||||
= link_to toggle_shared_runners_project_runners_path(@project), class: 'btn gl-button btn-success', method: :post do
|
||||
= _('Enable shared runners')
|
||||
for this project
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
- if defined?(text)
|
||||
= button_to text, '#', class: html_class, data: button_data
|
||||
- else
|
||||
= button_to '#', class: html_class, data: button_data do
|
||||
%span.sr-only= _('Delete')
|
||||
= sprite_icon('remove')
|
||||
|
|
@ -2239,6 +2239,14 @@
|
|||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: wikis_git_garbage_collect
|
||||
:feature_category: :gitaly
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: x509_certificate_revoke
|
||||
:feature_category: :source_code_management
|
||||
:has_external_dependencies:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GitGarbageCollectMethods
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include ApplicationWorker
|
||||
|
||||
sidekiq_options retry: false
|
||||
feature_category :gitaly
|
||||
loggable_arguments 1, 2, 3
|
||||
end
|
||||
|
||||
# Timeout set to 24h
|
||||
LEASE_TIMEOUT = 86400
|
||||
|
||||
def perform(resource_id, task = :gc, lease_key = nil, lease_uuid = nil)
|
||||
resource = find_resource(resource_id)
|
||||
lease_key ||= default_lease_key(task, resource)
|
||||
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
|
||||
|
||||
before_gitaly_call(task, resource)
|
||||
gitaly_call(task, resource)
|
||||
|
||||
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
|
||||
flush_ref_caches(resource) if gc?(task)
|
||||
|
||||
update_repository_statistics(resource) 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
|
||||
resource.cleanup
|
||||
ensure
|
||||
cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_lease_key(task, resource)
|
||||
"git_gc:#{task}:#{resource.class.name.underscore.pluralize}:#{resource.id}"
|
||||
end
|
||||
|
||||
def find_resource(id)
|
||||
raise NotImplementedError
|
||||
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 before_gitaly_call(task, resource)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def gitaly_call(task, resource)
|
||||
repository = resource.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 bitmaps_enabled?
|
||||
Gitlab::CurrentSettings.housekeeping_bitmaps_enabled
|
||||
end
|
||||
|
||||
def flush_ref_caches(resource)
|
||||
resource.repository.expire_branches_cache
|
||||
resource.repository.branch_names
|
||||
resource.repository.has_visible_content?
|
||||
end
|
||||
|
||||
def update_repository_statistics(resource)
|
||||
resource.repository.expire_statistics_caches
|
||||
|
||||
return if Gitlab::Database.read_only? # GitGarbageCollectWorker may be run on a Geo secondary
|
||||
|
||||
update_db_repository_statistics(resource)
|
||||
end
|
||||
|
||||
def update_db_repository_statistics(resource)
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
@ -2,131 +2,41 @@
|
|||
|
||||
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
|
||||
extend ::Gitlab::Utils::Override
|
||||
include GitGarbageCollectMethods
|
||||
|
||||
private
|
||||
|
||||
def find_project(project_id)
|
||||
Project.find(project_id)
|
||||
override :default_lease_key
|
||||
def default_lease_key(task, resource)
|
||||
"git_gc:#{task}:#{resource.id}"
|
||||
end
|
||||
|
||||
def gc?(task)
|
||||
task == :gc || task == :prune
|
||||
override :find_resource
|
||||
def find_resource(id)
|
||||
Project.find(id)
|
||||
end
|
||||
|
||||
def try_obtain_lease(key)
|
||||
::Gitlab::ExclusiveLease.new(key, timeout: LEASE_TIMEOUT).try_obtain
|
||||
override :before_gitaly_call
|
||||
def before_gitaly_call(task, resource)
|
||||
return unless gc?(task)
|
||||
|
||||
::Projects::GitDeduplicationService.new(resource).execute
|
||||
cleanup_orphan_lfs_file_references(resource)
|
||||
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)
|
||||
def cleanup_orphan_lfs_file_references(resource)
|
||||
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!
|
||||
::Gitlab::Cleanup::OrphanLfsFileReferences.new(resource, 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
|
||||
override :update_db_repository_statistics
|
||||
def update_db_repository_statistics(resource)
|
||||
Projects::UpdateStatisticsService.new(resource, nil, statistics: [:repository_size, :lfs_objects_size]).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Wikis
|
||||
class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
extend ::Gitlab::Utils::Override
|
||||
include GitGarbageCollectMethods
|
||||
|
||||
private
|
||||
|
||||
override :find_resource
|
||||
def find_resource(id)
|
||||
Project.find(id).wiki
|
||||
end
|
||||
|
||||
override :update_db_repository_statistics
|
||||
def update_db_repository_statistics(resource)
|
||||
Projects::UpdateStatisticsService.new(resource.container, nil, statistics: [:wiki_size]).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace angle-double-left icon with chevron-double-lg-left
|
||||
merge_request: 52393
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Convert project runner buttons to pajamas
|
||||
merge_request: 52358
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve the performance of merge request and issue search by label(s)
|
||||
merge_request: 52495
|
||||
author:
|
||||
type: performance
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: New Shared Partial for SSH Key Deletion
|
||||
merge_request: 50825
|
||||
author: Mehul Sharma
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix batch query issue when primary key is -1
|
||||
merge_request: 51716
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Track suggestion add/apply metrics
|
||||
merge_request: 52189
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add repository_read_only column to NamespaceSettings table
|
||||
merge_request: 52300
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Pass dependency proxy credentials to runners to log in automatically
|
||||
merge_request: 51927
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Apply new GitLab UI style to mirror update button and add space after icon
|
||||
merge_request: 51808
|
||||
author: Yogi (@yo)
|
||||
type: other
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: ci_instance_variables_ui
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33510
|
||||
rollout_issue_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299879
|
||||
milestone: '13.1'
|
||||
type: development
|
||||
group: group::continuous integration
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: ci_store_pipeline_messages
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33762
|
||||
rollout_issue_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/224199
|
||||
milestone: '13.2'
|
||||
type: development
|
||||
group: group::continuous integration
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ rollout_issue_url:
|
|||
milestone: '13.4'
|
||||
type: development
|
||||
group: group::analytics
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: usage_data_i_code_review_user_add_suggestion
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52189
|
||||
rollout_issue_url:
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: true
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: usage_data_i_code_review_user_apply_suggestion
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52189
|
||||
rollout_issue_url:
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: true
|
||||
|
|
@ -158,6 +158,8 @@
|
|||
- 1
|
||||
- - group_saml_group_sync
|
||||
- 1
|
||||
- - group_wikis_git_garbage_collect
|
||||
- 1
|
||||
- - hashed_storage
|
||||
- 1
|
||||
- - import_issues_csv
|
||||
|
|
@ -362,5 +364,7 @@
|
|||
- 1
|
||||
- - web_hooks_destroy
|
||||
- 1
|
||||
- - wikis_git_garbage_collect
|
||||
- 1
|
||||
- - x509_certificate_revoke
|
||||
- 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddRepositoryReadOnlyToNamespaceSettings < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_column :namespace_settings, :repository_read_only, :boolean, default: false, null: false
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column :namespace_settings, :repository_read_only
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
f5231b1eec17ea1a67f2d2f4ca759314afb85b2c8fb431e3303d530d44bdb1ef
|
||||
|
|
@ -14310,6 +14310,7 @@ CREATE TABLE namespace_settings (
|
|||
prevent_forking_outside_group boolean DEFAULT false NOT NULL,
|
||||
allow_mfa_for_subgroups boolean DEFAULT true NOT NULL,
|
||||
default_branch_name text,
|
||||
repository_read_only boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255))
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ This page describes the subfolders of the Kubernetes Agent repository.
|
|||
[Development information](index.md) and
|
||||
[end-user documentation](../../user/clusters/agent/index.md) are both available.
|
||||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For a video overview, see
|
||||
[GitLab Kubernetes Agent repository overview](https://www.youtube.com/watch?v=j8CyaCWroUY).
|
||||
|
||||
## `build`
|
||||
|
||||
Various files for the build process.
|
||||
|
|
@ -34,6 +38,16 @@ Each of these directories contain application bootstrap code for:
|
|||
- Constructing the dependency graph of objects that constitute the program.
|
||||
- Running it.
|
||||
|
||||
### `cmd/agentk`
|
||||
|
||||
- `agentk` initialization logic.
|
||||
- Implementation of the agent modules API.
|
||||
|
||||
### `cmd/kas`
|
||||
|
||||
- `kas` initialization logic.
|
||||
- Implementation of the server modules API.
|
||||
|
||||
## `examples`
|
||||
|
||||
Git submodules for the example projects.
|
||||
|
|
@ -42,10 +56,6 @@ Git submodules for the example projects.
|
|||
|
||||
The main code of both `gitlab-kas` and `agentk`, and various supporting building blocks.
|
||||
|
||||
### `internal/agentk`
|
||||
|
||||
Main `agentk` logic, including the API implementation for agent modules.
|
||||
|
||||
### `internal/api`
|
||||
|
||||
Structs that represent some important pieces of data.
|
||||
|
|
@ -58,12 +68,6 @@ Items to work with [Gitaly](../../administration/gitaly/index.md).
|
|||
|
||||
GitLab REST client.
|
||||
|
||||
### `internal/kas`
|
||||
|
||||
API implementation for the server modules. It contains nothing else, as all server logic
|
||||
is split into server modules. The bootstrapping glue that wires the modules together
|
||||
is in `cmd/kas/kasapp`.
|
||||
|
||||
### `internal/module`
|
||||
|
||||
Modules that implement server and agent-side functionality.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
stage: Growth
|
||||
group: Product Intelligence
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
<!---
|
||||
This documentation is auto generated by a script.
|
||||
|
||||
Please do not edit this file directly, check generate_metrics_dictionary task on lib/tasks/gitlab/usage_data.rake.
|
||||
--->
|
||||
|
||||
# Metrics Dictionary
|
||||
|
||||
This file is autogenerated, please do not edit directly.
|
||||
|
||||
To generate these files from the GitLab repository, run:
|
||||
|
||||
```shell
|
||||
bundle exec rake gitlab:usage_data:generate_metrics_dictionary
|
||||
```
|
||||
|
||||
The Metrics Dictionary is based on the following metrics definition YAML files:
|
||||
|
||||
- [`config/metrics`]('https://gitlab.com/gitlab-org/gitlab/-/tree/master/config/metrics')
|
||||
- [`ee/config/metrics`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/config/metrics)
|
||||
Each table includes a `milestone`, which corresponds to the GitLab version when the metric
|
||||
was released.
|
||||
|
||||
## counts.deployments
|
||||
|
||||
Total deployments count
|
||||
|
||||
| field | value |
|
||||
| --- | --- |
|
||||
| `key_path` | **counts.deployments** |
|
||||
| `value_type` | integer |
|
||||
| `stage` | release |
|
||||
| `status` | data_available |
|
||||
| `milestone` | 8.12 |
|
||||
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/735) |
|
||||
| `group` | `group::ops release` |
|
||||
| `time_frame` | all |
|
||||
| `data_source` | Database |
|
||||
| `distribution` | ee, ce |
|
||||
| `tier` | free, starter, premium, ultimate, bronze, silver, gold |
|
||||
|
||||
## counts.geo_nodes
|
||||
|
||||
Total number of sites in a Geo deployment
|
||||
|
||||
| field | value |
|
||||
| --- | --- |
|
||||
| `key_path` | **counts.geo_nodes** |
|
||||
| `value_type` | integer |
|
||||
| `product_category` | disaster_recovery |
|
||||
| `stage` | enablement |
|
||||
| `status` | data_available |
|
||||
| `milestone` | 11.2 |
|
||||
| `group` | `group::geo` |
|
||||
| `time_frame` | all |
|
||||
| `data_source` | Database |
|
||||
| `distribution` | ee |
|
||||
| `tier` | premium, ultimate |
|
||||
|
||||
## counts_monthy.deployments
|
||||
|
||||
Total deployments count for recent 28 days
|
||||
|
||||
| field | value |
|
||||
| --- | --- |
|
||||
| `key_path` | **counts_monthy.deployments** |
|
||||
| `value_type` | integer |
|
||||
| `stage` | release |
|
||||
| `status` | data_available |
|
||||
| `milestone` | 13.2 |
|
||||
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35493) |
|
||||
| `group` | `group::ops release` |
|
||||
| `time_frame` | 28d |
|
||||
| `data_source` | Database |
|
||||
| `distribution` | ee, ce |
|
||||
| `tier` | free, starter, premium, ultimate, bronze, silver, gold |
|
||||
|
||||
## database.adapter
|
||||
|
||||
This metric only returns a value of PostgreSQL in supported versions of GitLab. It could be removed from the usage ping. Historically MySQL was also supported.
|
||||
|
||||
| field | value |
|
||||
| --- | --- |
|
||||
| `key_path` | **database.adapter** |
|
||||
| `value_type` | string |
|
||||
| `product_category` | collection |
|
||||
| `stage` | growth |
|
||||
| `status` | data_available |
|
||||
| `group` | `group::enablement distribution` |
|
||||
| `time_frame` | none |
|
||||
| `data_source` | Database |
|
||||
| `distribution` | ee, ce |
|
||||
| `tier` | free, starter, premium, ultimate, bronze, silver, gold |
|
||||
|
||||
## recorded_at
|
||||
|
||||
When the Usage Ping computation was started
|
||||
|
||||
| field | value |
|
||||
| --- | --- |
|
||||
| `key_path` | **recorded_at** |
|
||||
| `value_type` | string |
|
||||
| `product_category` | collection |
|
||||
| `stage` | growth |
|
||||
| `status` | data_available |
|
||||
| `milestone` | 8.1 |
|
||||
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/557) |
|
||||
| `group` | `group::product analytics` |
|
||||
| `time_frame` | none |
|
||||
| `data_source` | Ruby |
|
||||
| `distribution` | ee, ce |
|
||||
| `tier` | free, starter, premium, ultimate, bronze, silver, gold |
|
||||
|
||||
## redis_hll_counters.issues_edit.g_project_management_issue_title_changed_weekly
|
||||
|
||||
Distinct users count that changed issue title in a group for last recent week
|
||||
|
||||
| field | value |
|
||||
| --- | --- |
|
||||
| `key_path` | **redis_hll_counters.issues_edit.g_project_management_issue_title_changed_weekly** |
|
||||
| `value_type` | integer |
|
||||
| `product_category` | issue_tracking |
|
||||
| `stage` | plan |
|
||||
| `status` | data_available |
|
||||
| `milestone` | 13.6 |
|
||||
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/issues/229918) |
|
||||
| `group` | `group::project management` |
|
||||
| `time_frame` | 7d |
|
||||
| `data_source` | Redis_hll |
|
||||
| `distribution` | ee, ce |
|
||||
| `tier` | free, starter, premium, ultimate, bronze, silver, gold |
|
||||
|
||||
## uuid
|
||||
|
||||
GitLab instance unique identifier
|
||||
|
||||
| field | value |
|
||||
| --- | --- |
|
||||
| `key_path` | **uuid** |
|
||||
| `value_type` | string |
|
||||
| `product_category` | collection |
|
||||
| `stage` | growth |
|
||||
| `status` | data_available |
|
||||
| `milestone` | 9.1 |
|
||||
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1521) |
|
||||
| `group` | `group::product analytics` |
|
||||
| `time_frame` | none |
|
||||
| `data_source` | Database |
|
||||
| `distribution` | ee, ce |
|
||||
| `tier` | free, starter, premium, ultimate, bronze, silver, gold |
|
||||
|
|
@ -225,6 +225,10 @@ There are different actions available to help triage and respond to incidents.
|
|||
Assign incidents to users that are actively responding. Select **Edit** in the
|
||||
right-hand side bar to select or deselect assignees.
|
||||
|
||||
### Associate a milestone
|
||||
|
||||
Associate an incident to a milestone by selecting **Edit** next to the milestone feature in the right-hand side bar.
|
||||
|
||||
### Change severity
|
||||
|
||||
See [Incident List](#incident-list) for a full description of the severity levels available.
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ values extracted from the [`alerts` field in webhook payload](https://prometheus
|
|||
|
||||
- Issue author: `GitLab Alert Bot`
|
||||
- Issue title: Extracted from the alert payload fields `annotations/title`, `annotations/summary`, or `labels/alertname`.
|
||||
- Issue description: Extracted from alert payload field `annotations/description`.
|
||||
- Alert `Summary`: A list of properties from the alert's payload.
|
||||
- `starts_at`: Alert start time from the payload's `startsAt` field
|
||||
- `full_query`: Alert query extracted from the payload's `generatorURL` field
|
||||
|
|
|
|||
|
|
@ -44,8 +44,18 @@ provided as part of your GitLab installation.
|
|||
To do so, add the following to your `.gitlab-ci.yml` file:
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- fuzz
|
||||
|
||||
include:
|
||||
- template: Coverage-Fuzzing.gitlab-ci.yml
|
||||
|
||||
my_fuzz_target:
|
||||
extends: .fuzz_base
|
||||
script:
|
||||
# Build your fuzz target binary in these steps, then run it with gitlab-cov-fuzz>
|
||||
# See our example repos for how you could do this with any of our supported languages
|
||||
- ./gitlab-cov-fuzz run --regression=$REGRESSION -- <your fuzz target>
|
||||
```
|
||||
|
||||
The included template makes available the [hidden job](../../../ci/yaml/README.md#hide-jobs)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ to the project, provided the cluster is not disabled.
|
|||
|
||||
## Multiple Kubernetes clusters
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35094) to GitLab Core in 13.2.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35094) in GitLab Core 13.2.
|
||||
|
||||
You can associate more than one Kubernetes cluster to your group, and maintain different clusters
|
||||
for different environments, such as development, staging, and production.
|
||||
|
|
|
|||
|
|
@ -91,33 +91,29 @@ You can authenticate using:
|
|||
|
||||
#### Authenticate within CI/CD
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/280582) in GitLab 13.7.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/280582) in GitLab 13.7.
|
||||
> - Automatic runner authentication [added](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27302) in GitLab 13.9
|
||||
|
||||
To work with the Dependency Proxy in [GitLab CI/CD](../../../ci/README.md), you can use:
|
||||
Runners will log into the Dependency Proxy automatically. We can pull through
|
||||
the dependency proxy using the `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX`
|
||||
environment variable:
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/node:latest
|
||||
```
|
||||
|
||||
There are other additional predefined environment variables we can also use:
|
||||
|
||||
- `CI_DEPENDENCY_PROXY_USER`: A CI user for logging in to the Dependency Proxy.
|
||||
- `CI_DEPENDENCY_PROXY_PASSWORD`: A CI password for logging in to the Dependency Proxy.
|
||||
- `CI_DEPENDENCY_PROXY_SERVER`: The server for logging in to the Dependency Proxy.
|
||||
- `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX`: The image prefix for pulling images through the Dependency Proxy.
|
||||
|
||||
This script shows how to use these variables to log in and pull an image from the Dependency Proxy:
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
|
||||
dependency-proxy-pull-master:
|
||||
# Official docker image.
|
||||
image: docker:latest
|
||||
stage: build
|
||||
services:
|
||||
- docker:dind
|
||||
before_script:
|
||||
- docker login -u "$CI_DEPENDENCY_PROXY_USER" -p "$CI_DEPENDENCY_PROXY_PASSWORD" "$CI_DEPENDENCY_PROXY_SERVER"
|
||||
script:
|
||||
- docker pull "$CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX"/alpine:latest
|
||||
```
|
||||
|
||||
`CI_DEPENDENCY_PROXY_SERVER` and `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX` include the server port. So if you use `CI_DEPENDENCY_PROXY_SERVER` to log in, for example, you must explicitly include the port in your pull command and vice-versa:
|
||||
`CI_DEPENDENCY_PROXY_SERVER` and `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX`
|
||||
include the server port. So if you explicitly include the Dependency Proxy
|
||||
path, the port must be included unless you have logged into the dependency
|
||||
proxy manually without including the port:
|
||||
|
||||
```shell
|
||||
docker pull gitlab.example.com:443/my-group/dependency_proxy/containers/alpine:latest
|
||||
|
|
@ -125,61 +121,6 @@ docker pull gitlab.example.com:443/my-group/dependency_proxy/containers/alpine:l
|
|||
|
||||
You can also use [custom environment variables](../../../ci/variables/README.md#custom-environment-variables) to store and access your personal access token or other valid credentials.
|
||||
|
||||
##### Authenticate with `DOCKER_AUTH_CONFIG`
|
||||
|
||||
You can use the Dependency Proxy to pull your base image.
|
||||
|
||||
1. [Create a `DOCKER_AUTH_CONFIG` environment variable](../../../ci/docker/using_docker_images.md#define-an-image-from-a-private-container-registry).
|
||||
1. Get credentials that allow you to log into the Dependency Proxy.
|
||||
1. Generate the version of these credentials that will be used by Docker:
|
||||
|
||||
```shell
|
||||
# The use of "-n" - prevents encoding a newline in the password.
|
||||
echo -n "my_username:my_password" | base64
|
||||
|
||||
# Example output to copy
|
||||
bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=
|
||||
```
|
||||
|
||||
This can also be a [personal access token](../../../user/profile/personal_access_tokens.md) such as:
|
||||
|
||||
```shell
|
||||
echo -n "my_username:personal_access_token" | base64
|
||||
```
|
||||
|
||||
1. Create a [custom environment variables](../../../ci/variables/README.md#custom-environment-variables)
|
||||
named `DOCKER_AUTH_CONFIG` with a value of:
|
||||
|
||||
```json
|
||||
{
|
||||
"auths": {
|
||||
"https://gitlab.example.com": {
|
||||
"auth": "(Base64 content from above)"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To use `$CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX` when referencing images, you must explicitly include the port in your `DOCKER_AUTH_CONFIG` value:
|
||||
|
||||
```json
|
||||
{
|
||||
"auths": {
|
||||
"https://gitlab.example.com:443": {
|
||||
"auth": "(Base64 content from above)"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Now reference the Dependency Proxy in your base image:
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/node:latest
|
||||
...
|
||||
```
|
||||
|
||||
### Store a Docker image in Dependency Proxy cache
|
||||
|
||||
To store a Docker image in Dependency Proxy storage:
|
||||
|
|
|
|||
|
|
@ -42,8 +42,12 @@ complete, the merge is blocked until you resolve all existing threads.
|
|||
|
||||
## Only allow merge requests to be merged if the pipeline succeeds
|
||||
|
||||
You can prevent merge requests from being merged if their pipeline did not succeed
|
||||
or if there are threads to be resolved. This works for both:
|
||||
You can prevent merge requests from being merged if:
|
||||
|
||||
- No pipeline ran.
|
||||
- The pipeline did not succeed.
|
||||
|
||||
This works for both:
|
||||
|
||||
- GitLab CI/CD pipelines
|
||||
- Pipelines run from an [external CI integration](../integrations/overview.md#integrations-listing)
|
||||
|
|
@ -58,6 +62,7 @@ CI providers with this feature. To enable it, you must:
|
|||
1. Press **Save** for the changes to take effect.
|
||||
|
||||
This setting also prevents merge requests from being merged if there is no pipeline.
|
||||
You should be careful to configure CI/CD so that pipelines run for every merge request.
|
||||
|
||||
### Limitations
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module Gitlab
|
|||
module Credentials
|
||||
class Base
|
||||
def type
|
||||
self.class.name.demodulize.underscore
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def providers
|
||||
[Registry]
|
||||
[Registry::GitlabRegistry, Registry::DependencyProxy]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Build
|
||||
module Credentials
|
||||
class Registry < Base
|
||||
attr_reader :username, :password
|
||||
|
||||
def initialize(build)
|
||||
@username = 'gitlab-ci-token'
|
||||
@password = build.token
|
||||
end
|
||||
|
||||
def url
|
||||
Gitlab.config.registry.host_port
|
||||
end
|
||||
|
||||
def valid?
|
||||
Gitlab.config.registry.enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Build
|
||||
module Credentials
|
||||
module Registry
|
||||
class DependencyProxy < GitlabRegistry
|
||||
def url
|
||||
"#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}"
|
||||
end
|
||||
|
||||
def valid?
|
||||
Gitlab.config.dependency_proxy.enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Build
|
||||
module Credentials
|
||||
module Registry
|
||||
class GitlabRegistry < Credentials::Base
|
||||
attr_reader :username, :password
|
||||
|
||||
def initialize(build)
|
||||
@username = Gitlab::Auth::CI_JOB_USER
|
||||
@password = build.token
|
||||
end
|
||||
|
||||
def url
|
||||
Gitlab.config.registry.host_port
|
||||
end
|
||||
|
||||
def valid?
|
||||
Gitlab.config.registry.enabled
|
||||
end
|
||||
|
||||
def type
|
||||
'registry'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Usage
|
||||
module Docs
|
||||
# Helper with functions to be used by HAML templates
|
||||
module Helper
|
||||
HEADER = %w(field value).freeze
|
||||
SKIP_KEYS = %i(description).freeze
|
||||
|
||||
def auto_generated_comment
|
||||
<<-MARKDOWN.strip_heredoc
|
||||
---
|
||||
stage: Growth
|
||||
group: Product Intelligence
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
<!---
|
||||
This documentation is auto generated by a script.
|
||||
|
||||
Please do not edit this file directly, check generate_metrics_dictionary task on lib/tasks/gitlab/usage_data.rake.
|
||||
--->
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
def render_name(name)
|
||||
"## #{name}\n"
|
||||
end
|
||||
|
||||
def render_description(object)
|
||||
object.description
|
||||
end
|
||||
|
||||
def render_attribute_row(key, value)
|
||||
value = Gitlab::Usage::Docs::ValueFormatter.format(key, value)
|
||||
table_row(["`#{key}`", value])
|
||||
end
|
||||
|
||||
def render_attributes_table(object)
|
||||
<<~MARKDOWN
|
||||
|
||||
#{table_row(HEADER)}
|
||||
#{table_row(HEADER.map { '---' })}
|
||||
#{table_value_rows(object.attributes)}
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
def table_value_rows(attributes)
|
||||
attributes.reject { |k, _| k.in?(SKIP_KEYS) }.map do |key, value|
|
||||
render_attribute_row(key, value)
|
||||
end.join("\n")
|
||||
end
|
||||
|
||||
def table_row(array)
|
||||
"| #{array.join(' | ')} |"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Usage
|
||||
module Docs
|
||||
class Renderer
|
||||
include Gitlab::Usage::Docs::Helper
|
||||
DICTIONARY_PATH = Rails.root.join('doc', 'development', 'usage_ping')
|
||||
TEMPLATE_PATH = Rails.root.join('lib', 'gitlab', 'usage', 'docs', 'templates', 'default.md.haml')
|
||||
|
||||
def initialize(metrics_definitions)
|
||||
@layout = Haml::Engine.new(File.read(TEMPLATE_PATH))
|
||||
@metrics_definitions = metrics_definitions.sort
|
||||
end
|
||||
|
||||
def contents
|
||||
# Render and remove an extra trailing new line
|
||||
@contents ||= @layout.render(self, metrics_definitions: @metrics_definitions).sub!(/\n(?=\Z)/, '')
|
||||
end
|
||||
|
||||
def write
|
||||
filename = DICTIONARY_PATH.join('dictionary.md').to_s
|
||||
|
||||
FileUtils.mkdir_p(DICTIONARY_PATH)
|
||||
File.write(filename, contents)
|
||||
|
||||
filename
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
= auto_generated_comment
|
||||
|
||||
:plain
|
||||
# Metrics Dictionary
|
||||
|
||||
This file is autogenerated, please do not edit directly.
|
||||
|
||||
To generate these files from the GitLab repository, run:
|
||||
|
||||
```shell
|
||||
bundle exec rake gitlab:usage_data:generate_metrics_dictionary
|
||||
```
|
||||
|
||||
The Metrics Dictionary is based on the following metrics definition YAML files:
|
||||
|
||||
- [`config/metrics`]('https://gitlab.com/gitlab-org/gitlab/-/tree/master/config/metrics')
|
||||
- [`ee/config/metrics`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/config/metrics)
|
||||
|
||||
Each table includes a `milestone`, which corresponds to the GitLab version when the metric
|
||||
was released.
|
||||
\
|
||||
- metrics_definitions.each do |name, object|
|
||||
|
||||
= render_name(name)
|
||||
|
||||
= render_description(object)
|
||||
|
||||
= render_attributes_table(object)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Usage
|
||||
module Docs
|
||||
class ValueFormatter
|
||||
def self.format(key, value)
|
||||
case key
|
||||
when :key_path
|
||||
"**#{value}**"
|
||||
when :data_source
|
||||
value.capitalize
|
||||
when :group
|
||||
"`#{value}`"
|
||||
when :introduced_by_url
|
||||
"[Introduced by](#{value})"
|
||||
when :distribution, :tier
|
||||
Array(value).join(', ')
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -521,6 +521,16 @@
|
|||
category: code_review
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_i_code_review_user_remove_multiline_mr_comment
|
||||
- name: i_code_review_user_add_suggestion
|
||||
redis_slot: code_review
|
||||
category: code_review
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_i_code_review_user_add_suggestion
|
||||
- name: i_code_review_user_apply_suggestion
|
||||
redis_slot: code_review
|
||||
category: code_review
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_i_code_review_user_apply_suggestion
|
||||
# Terraform
|
||||
- name: p_terraform_state_api_unique_users
|
||||
category: terraform
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ module Gitlab
|
|||
MR_CREATE_MULTILINE_COMMENT_ACTION = 'i_code_review_user_create_multiline_mr_comment'
|
||||
MR_EDIT_MULTILINE_COMMENT_ACTION = 'i_code_review_user_edit_multiline_mr_comment'
|
||||
MR_REMOVE_MULTILINE_COMMENT_ACTION = 'i_code_review_user_remove_multiline_mr_comment'
|
||||
MR_ADD_SUGGESTION_ACTION = 'i_code_review_user_add_suggestion'
|
||||
MR_APPLY_SUGGESTION_ACTION = 'i_code_review_user_apply_suggestion'
|
||||
|
||||
class << self
|
||||
def track_mr_diffs_action(merge_request:)
|
||||
|
|
@ -68,6 +70,14 @@ module Gitlab
|
|||
track_unique_action_by_user(MR_PUBLISH_REVIEW_ACTION, user)
|
||||
end
|
||||
|
||||
def track_add_suggestion_action(user:)
|
||||
track_unique_action_by_user(MR_ADD_SUGGESTION_ACTION, user)
|
||||
end
|
||||
|
||||
def track_apply_suggestion_action(user:)
|
||||
track_unique_action_by_user(MR_APPLY_SUGGESTION_ACTION, user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def track_unique_action_by_merge_request(action, merge_request)
|
||||
|
|
|
|||
|
|
@ -21,5 +21,11 @@ namespace :gitlab do
|
|||
|
||||
puts Gitlab::Json.pretty_generate(result.attributes)
|
||||
end
|
||||
|
||||
desc 'GitLab | UsageData | Generate metrics dictionary'
|
||||
task generate_metrics_dictionary: :environment do
|
||||
items = Gitlab::Usage::MetricDefinition.definitions
|
||||
Gitlab::Usage::Docs::Renderer.new(items).write
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15410,6 +15410,9 @@ msgstr ""
|
|||
msgid "Integrations|Enable comments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Failed to link namespace. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Failed to load namespaces. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16986,6 +16989,9 @@ msgstr[1] ""
|
|||
msgid "Line changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Link"
|
||||
msgstr ""
|
||||
|
||||
msgid "Link Prometheus monitoring to GitLab."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,21 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :unauthenticated do
|
||||
author_id { -1 }
|
||||
details do
|
||||
{
|
||||
custom_message: 'Custom action',
|
||||
author_name: 'An unauthenticated user',
|
||||
target_id: target_project.id,
|
||||
target_type: 'Project',
|
||||
target_details: target_project.name,
|
||||
ip_address: '127.0.0.1',
|
||||
entity_path: target_project.full_path
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :group_event do
|
||||
transient { target_group { association(:group) } }
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ describe('JiraConnect API', () => {
|
|||
const mockJwt = 'jwt';
|
||||
const mockResponse = { success: true };
|
||||
|
||||
const tokenSpy = jest.fn().mockReturnValue(mockJwt);
|
||||
const tokenSpy = jest.fn((callback) => callback(mockJwt));
|
||||
|
||||
window.AP = {
|
||||
context: {
|
||||
|
|
|
|||
|
|
@ -1,27 +1,38 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlAvatar } from '@gitlab/ui';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import { GlAvatar, GlButton } from '@gitlab/ui';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
import { mockGroup1 } from '../mock_data';
|
||||
|
||||
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
|
||||
import * as JiraConnectApi from '~/jira_connect/api';
|
||||
|
||||
describe('GroupsListItem', () => {
|
||||
let wrapper;
|
||||
const mockSubscriptionPath = 'subscriptionPath';
|
||||
|
||||
const createComponent = () => {
|
||||
const reloadSpy = jest.fn();
|
||||
|
||||
global.AP = {
|
||||
navigator: {
|
||||
reload: reloadSpy,
|
||||
},
|
||||
};
|
||||
|
||||
const createComponent = ({ mountFn = shallowMount } = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
shallowMount(GroupsListItem, {
|
||||
mountFn(GroupsListItem, {
|
||||
propsData: {
|
||||
group: mockGroup1,
|
||||
},
|
||||
provide: {
|
||||
subscriptionsPath: mockSubscriptionPath,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
|
|
@ -30,17 +41,82 @@ describe('GroupsListItem', () => {
|
|||
const findGlAvatar = () => wrapper.find(GlAvatar);
|
||||
const findGroupName = () => wrapper.findByTestId('group-list-item-name');
|
||||
const findGroupDescription = () => wrapper.findByTestId('group-list-item-description');
|
||||
const findLinkButton = () => wrapper.find(GlButton);
|
||||
const clickLinkButton = () => findLinkButton().trigger('click');
|
||||
|
||||
it('renders group avatar', () => {
|
||||
expect(findGlAvatar().exists()).toBe(true);
|
||||
expect(findGlAvatar().props('src')).toBe(mockGroup1.avatar_url);
|
||||
describe('template', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders group avatar', () => {
|
||||
expect(findGlAvatar().exists()).toBe(true);
|
||||
expect(findGlAvatar().props('src')).toBe(mockGroup1.avatar_url);
|
||||
});
|
||||
|
||||
it('renders group name', () => {
|
||||
expect(findGroupName().text()).toBe(mockGroup1.full_name);
|
||||
});
|
||||
|
||||
it('renders group description', () => {
|
||||
expect(findGroupDescription().text()).toBe(mockGroup1.description);
|
||||
});
|
||||
|
||||
it('renders Link button', () => {
|
||||
expect(findLinkButton().exists()).toBe(true);
|
||||
expect(findLinkButton().text()).toBe('Link');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders group name', () => {
|
||||
expect(findGroupName().text()).toBe(mockGroup1.full_name);
|
||||
});
|
||||
describe('on Link button click', () => {
|
||||
let addSubscriptionSpy;
|
||||
|
||||
it('renders group description', () => {
|
||||
expect(findGroupDescription().text()).toBe(mockGroup1.description);
|
||||
beforeEach(() => {
|
||||
createComponent({ mountFn: mount });
|
||||
|
||||
addSubscriptionSpy = jest.spyOn(JiraConnectApi, 'addSubscription').mockResolvedValue();
|
||||
});
|
||||
|
||||
it('sets button to loading and sends request', async () => {
|
||||
expect(findLinkButton().props('loading')).toBe(false);
|
||||
|
||||
clickLinkButton();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findLinkButton().props('loading')).toBe(true);
|
||||
|
||||
expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path);
|
||||
});
|
||||
|
||||
describe('when request is successful', () => {
|
||||
it('reloads the page', async () => {
|
||||
clickLinkButton();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(reloadSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when request has errors', () => {
|
||||
const mockErrorMessage = 'error message';
|
||||
const mockError = { response: { data: { error: mockErrorMessage } } };
|
||||
|
||||
beforeEach(() => {
|
||||
addSubscriptionSpy = jest
|
||||
.spyOn(JiraConnectApi, 'addSubscription')
|
||||
.mockRejectedValue(mockError);
|
||||
});
|
||||
|
||||
it('emits `error` event', async () => {
|
||||
clickLinkButton();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(reloadSpy).not.toHaveBeenCalled();
|
||||
expect(wrapper.emitted('error')[0][0]).toBe(mockErrorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
import { fetchGroups } from '~/jira_connect/api';
|
||||
|
|
@ -28,6 +28,7 @@ describe('GroupsList', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
const findGlAlert = () => wrapper.find(GlAlert);
|
||||
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const findAllItems = () => wrapper.findAll(GroupsListItem);
|
||||
const findFirstItem = () => findAllItems().at(0);
|
||||
|
|
@ -45,6 +46,18 @@ describe('GroupsList', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('error fetching groups', () => {
|
||||
it('renders error message', async () => {
|
||||
fetchGroups.mockRejectedValue();
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findGlAlert().exists()).toBe(true);
|
||||
expect(findGlAlert().text()).toBe('Failed to load namespaces. Please try again.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('no groups returned', () => {
|
||||
it('renders empty state', async () => {
|
||||
fetchGroups.mockResolvedValue(mockEmptyResponse);
|
||||
|
|
@ -57,15 +70,28 @@ describe('GroupsList', () => {
|
|||
});
|
||||
|
||||
describe('with groups returned', () => {
|
||||
it('renders groups list', async () => {
|
||||
beforeEach(async () => {
|
||||
fetchGroups.mockResolvedValue({ data: [mockGroup1, mockGroup2] });
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('renders groups list', () => {
|
||||
expect(findAllItems().length).toBe(2);
|
||||
expect(findFirstItem().props('group')).toBe(mockGroup1);
|
||||
expect(findSecondItem().props('group')).toBe(mockGroup2);
|
||||
});
|
||||
|
||||
it('shows error message on $emit from item', async () => {
|
||||
const errorMessage = 'error message';
|
||||
|
||||
findFirstItem().vm.$emit('error', errorMessage);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findGlAlert().exists()).toBe(true);
|
||||
expect(findGlAlert().text()).toContain(errorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export const mockGroup1 = {
|
|||
avatar_url: 'avatar.png',
|
||||
name: 'Gitlab Org',
|
||||
full_name: 'Gitlab Org',
|
||||
full_path: 'gitlab-org',
|
||||
description: 'Open source software to collaborate on code',
|
||||
};
|
||||
|
||||
|
|
@ -11,5 +12,6 @@ export const mockGroup2 = {
|
|||
avatar_url: 'avatar.png',
|
||||
name: 'Gitlab Com',
|
||||
full_name: 'Gitlab Com',
|
||||
full_path: 'gitlab-com',
|
||||
description: 'For GitLab company related projects',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,12 +4,21 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe JiraConnectHelper do
|
||||
describe '#jira_connect_app_data' do
|
||||
subject { helper.jira_connect_app_data }
|
||||
let_it_be(:subscription) { create(:jira_connect_subscription) }
|
||||
|
||||
subject { helper.jira_connect_app_data([subscription]) }
|
||||
|
||||
it 'includes Jira Connect app attributes' do
|
||||
is_expected.to include(
|
||||
:groups_path
|
||||
:groups_path,
|
||||
:subscriptions_path
|
||||
)
|
||||
end
|
||||
|
||||
it 'passes group as "skip_groups" param' do
|
||||
skip_groups_param = CGI.escape('skip_groups[]')
|
||||
|
||||
expect(subject[:groups_path]).to include("#{skip_groups_param}=#{subscription.namespace.id}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Build::Credentials::Registry::DependencyProxy do
|
||||
let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let(:gitlab_url) { 'gitlab.example.com:443' }
|
||||
|
||||
subject { described_class.new(build) }
|
||||
|
||||
before do
|
||||
stub_config_setting(host: 'gitlab.example.com', port: 443)
|
||||
end
|
||||
|
||||
it 'contains valid dependency proxy credentials' do
|
||||
expect(subject).to be_kind_of(described_class)
|
||||
|
||||
expect(subject.username).to eq 'gitlab-ci-token'
|
||||
expect(subject.password).to eq build.token
|
||||
expect(subject.url).to eq gitlab_url
|
||||
expect(subject.type).to eq 'registry'
|
||||
end
|
||||
|
||||
describe '.valid?' do
|
||||
subject { described_class.new(build).valid? }
|
||||
|
||||
context 'when dependency proxy is enabled' do
|
||||
before do
|
||||
stub_config(dependency_proxy: { enabled: true })
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when dependency proxy is disabled' do
|
||||
before do
|
||||
stub_config(dependency_proxy: { enabled: false })
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Build::Credentials::Registry do
|
||||
RSpec.describe Gitlab::Ci::Build::Credentials::Registry::GitlabRegistry do
|
||||
let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let(:registry_url) { 'registry.example.com:5005' }
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Usage::Docs::Renderer do
|
||||
describe 'contents' do
|
||||
let(:dictionary_path) { Gitlab::Usage::Docs::Renderer::DICTIONARY_PATH }
|
||||
let(:items) { Gitlab::Usage::MetricDefinition.definitions }
|
||||
|
||||
it 'generates dictionary for given items' do
|
||||
generated_dictionary = described_class.new(items).contents
|
||||
generated_dictionary_keys = RDoc::Markdown
|
||||
.parse(generated_dictionary)
|
||||
.table_of_contents
|
||||
.select { |metric_doc| metric_doc.level == 2 && !metric_doc.text.start_with?('info:') }
|
||||
.map(&:text)
|
||||
|
||||
expect(generated_dictionary_keys).to match_array(items.keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Usage::Docs::ValueFormatter do
|
||||
describe '.format' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
where(:key, :value, :expected_value) do
|
||||
:group | 'growth::product intelligence' | '`growth::product intelligence`'
|
||||
:data_source | 'redis' | 'Redis'
|
||||
:data_source | 'ruby' | 'Ruby'
|
||||
:introduced_by_url | 'http://test.com' | '[Introduced by](http://test.com)'
|
||||
:tier | %w(gold premium) | 'gold, premium'
|
||||
:distribution | %w(ce ee) | 'ce, ee'
|
||||
:key_path | 'key.path' | '**key.path**'
|
||||
:milestone | '13.4' | '13.4'
|
||||
:status | 'data_available' | 'data_available'
|
||||
end
|
||||
|
||||
with_them do
|
||||
subject { described_class.format(key, value) }
|
||||
|
||||
it { is_expected.to eq(expected_value) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -148,4 +148,20 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
|
|||
let(:action) { described_class::MR_PUBLISH_REVIEW_ACTION }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.track_add_suggestion_action' do
|
||||
subject { described_class.track_add_suggestion_action(user: user) }
|
||||
|
||||
it_behaves_like 'a tracked merge request unique event' do
|
||||
let(:action) { described_class::MR_ADD_SUGGESTION_ACTION }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.track_apply_suggestion_action' do
|
||||
subject { described_class.track_apply_suggestion_action(user: user) }
|
||||
|
||||
it_behaves_like 'a tracked merge request unique event' do
|
||||
let(:action) { described_class::MR_APPLY_SUGGESTION_ACTION }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2999,6 +2999,7 @@ RSpec.describe Project, factory_default: :keep do
|
|||
it_behaves_like 'can housekeep repository' do
|
||||
let(:resource) { build_stubbed(:project) }
|
||||
let(:resource_key) { 'projects' }
|
||||
let(:expected_worker_class) { Projects::GitGarbageCollectWorker }
|
||||
end
|
||||
|
||||
describe '#deployment_variables' do
|
||||
|
|
|
|||
|
|
@ -47,5 +47,6 @@ RSpec.describe ProjectWiki do
|
|||
let_it_be(:resource) { create(:project_wiki) }
|
||||
|
||||
let(:resource_key) { 'project_wikis' }
|
||||
let(:expected_worker_class) { Wikis::GitGarbageCollectWorker }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -74,6 +74,14 @@ RSpec.describe Suggestions::ApplyService do
|
|||
expect(commit.author_name).to eq(user.name)
|
||||
end
|
||||
|
||||
it 'tracks apply suggestion event' do
|
||||
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
|
||||
.to receive(:track_apply_suggestion_action)
|
||||
.with(user: user)
|
||||
|
||||
apply(suggestions)
|
||||
end
|
||||
|
||||
context 'when a custom suggestion commit message' do
|
||||
before do
|
||||
project.update!(suggestion_commit_message: message)
|
||||
|
|
@ -570,56 +578,84 @@ RSpec.describe Suggestions::ApplyService do
|
|||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
context 'diff file was not found' do
|
||||
it 'returns error message' do
|
||||
expect(suggestion.note).to receive(:latest_diff_file) { nil }
|
||||
shared_examples_for 'service not tracking apply suggestion event' do
|
||||
it 'does not track apply suggestion event' do
|
||||
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
|
||||
.not_to receive(:track_apply_suggestion_action)
|
||||
|
||||
result = apply_service.new(user, suggestion).execute
|
||||
|
||||
expect(result).to eq(message: 'A file was not found.',
|
||||
status: :error)
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
context 'diff file was not found' do
|
||||
let(:result) { apply_service.new(user, suggestion).execute }
|
||||
|
||||
before do
|
||||
expect(suggestion.note).to receive(:latest_diff_file) { nil }
|
||||
end
|
||||
|
||||
it 'returns error message' do
|
||||
expect(result).to eq(message: 'A file was not found.',
|
||||
status: :error)
|
||||
end
|
||||
|
||||
it_behaves_like 'service not tracking apply suggestion event'
|
||||
end
|
||||
|
||||
context 'when not all suggestions belong to the same branch' do
|
||||
let(:merge_request2) do
|
||||
create(
|
||||
:merge_request,
|
||||
:conflict,
|
||||
source_project: project,
|
||||
target_project: project
|
||||
)
|
||||
end
|
||||
|
||||
let(:position2) do
|
||||
Gitlab::Diff::Position.new(
|
||||
old_path: "files/ruby/popen.rb",
|
||||
new_path: "files/ruby/popen.rb",
|
||||
old_line: nil,
|
||||
new_line: 15,
|
||||
diff_refs: merge_request2.diff_refs
|
||||
)
|
||||
end
|
||||
|
||||
let(:diff_note2) do
|
||||
create(
|
||||
:diff_note_on_merge_request,
|
||||
noteable: merge_request2,
|
||||
position: position2,
|
||||
project: project
|
||||
)
|
||||
end
|
||||
|
||||
let(:other_branch_suggestion) { create(:suggestion, note: diff_note2) }
|
||||
let(:result) { apply_service.new(user, suggestion, other_branch_suggestion).execute }
|
||||
|
||||
it 'renders error message' do
|
||||
merge_request2 = create(:merge_request,
|
||||
:conflict,
|
||||
source_project: project,
|
||||
target_project: project)
|
||||
|
||||
position2 = Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
|
||||
new_path: "files/ruby/popen.rb",
|
||||
old_line: nil,
|
||||
new_line: 15,
|
||||
diff_refs: merge_request2
|
||||
.diff_refs)
|
||||
|
||||
diff_note2 = create(:diff_note_on_merge_request,
|
||||
noteable: merge_request2,
|
||||
position: position2,
|
||||
project: project)
|
||||
|
||||
other_branch_suggestion = create(:suggestion, note: diff_note2)
|
||||
|
||||
result = apply_service.new(user, suggestion, other_branch_suggestion).execute
|
||||
|
||||
expect(result).to eq(message: 'Suggestions must all be on the same branch.',
|
||||
status: :error)
|
||||
end
|
||||
|
||||
it_behaves_like 'service not tracking apply suggestion event'
|
||||
end
|
||||
|
||||
context 'suggestion is not appliable' do
|
||||
let(:inapplicable_reason) { "Can't apply this suggestion." }
|
||||
let(:result) { apply_service.new(user, suggestion).execute }
|
||||
|
||||
it 'returns error message' do
|
||||
before do
|
||||
expect(suggestion).to receive(:appliable?).and_return(false)
|
||||
expect(suggestion).to receive(:inapplicable_reason).and_return(inapplicable_reason)
|
||||
end
|
||||
|
||||
result = apply_service.new(user, suggestion).execute
|
||||
|
||||
it 'returns error message' do
|
||||
expect(result).to eq(message: inapplicable_reason, status: :error)
|
||||
end
|
||||
|
||||
it_behaves_like 'service not tracking apply suggestion event'
|
||||
end
|
||||
|
||||
context 'lines of suggestions overlap' do
|
||||
|
|
@ -632,12 +668,14 @@ RSpec.describe Suggestions::ApplyService do
|
|||
create_suggestion(to_content: "I Overlap!")
|
||||
end
|
||||
|
||||
it 'returns error message' do
|
||||
result = apply_service.new(user, suggestion, overlapping_suggestion).execute
|
||||
let(:result) { apply_service.new(user, suggestion, overlapping_suggestion).execute }
|
||||
|
||||
it 'returns error message' do
|
||||
expect(result).to eq(message: 'Suggestions are not applicable as their lines cannot overlap.',
|
||||
status: :error)
|
||||
end
|
||||
|
||||
it_behaves_like 'service not tracking apply suggestion event'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -53,6 +53,15 @@ RSpec.describe Suggestions::CreateService do
|
|||
|
||||
subject { described_class.new(note) }
|
||||
|
||||
shared_examples_for 'service not tracking add suggestion event' do
|
||||
it 'does not track add suggestion event' do
|
||||
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
|
||||
.not_to receive(:track_add_suggestion_action)
|
||||
|
||||
subject.execute
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'should not try to parse suggestions' do
|
||||
context 'when not a diff note for merge requests' do
|
||||
|
|
@ -66,6 +75,8 @@ RSpec.describe Suggestions::CreateService do
|
|||
|
||||
subject.execute
|
||||
end
|
||||
|
||||
it_behaves_like 'service not tracking add suggestion event'
|
||||
end
|
||||
|
||||
context 'when diff note is not for text' do
|
||||
|
|
@ -76,17 +87,21 @@ RSpec.describe Suggestions::CreateService do
|
|||
note: markdown)
|
||||
end
|
||||
|
||||
it 'does not try to parse suggestions' do
|
||||
before do
|
||||
allow(note).to receive(:on_text?) { false }
|
||||
end
|
||||
|
||||
it 'does not try to parse suggestions' do
|
||||
expect(Gitlab::Diff::SuggestionsParser).not_to receive(:parse)
|
||||
|
||||
subject.execute
|
||||
end
|
||||
|
||||
it_behaves_like 'service not tracking add suggestion event'
|
||||
end
|
||||
end
|
||||
|
||||
context 'should not create suggestions' do
|
||||
context 'when diff file is not found' do
|
||||
let(:note) do
|
||||
create(:diff_note_on_merge_request, project: project_with_repo,
|
||||
noteable: merge_request,
|
||||
|
|
@ -94,13 +109,17 @@ RSpec.describe Suggestions::CreateService do
|
|||
note: markdown)
|
||||
end
|
||||
|
||||
it 'creates no suggestion when diff file is not found' do
|
||||
before do
|
||||
expect_next_instance_of(DiffNote) do |diff_note|
|
||||
expect(diff_note).to receive(:latest_diff_file).once { nil }
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates no suggestion' do
|
||||
expect { subject.execute }.not_to change(Suggestion, :count)
|
||||
end
|
||||
|
||||
it_behaves_like 'service not tracking add suggestion event'
|
||||
end
|
||||
|
||||
context 'should create suggestions' do
|
||||
|
|
@ -137,6 +156,14 @@ RSpec.describe Suggestions::CreateService do
|
|||
end
|
||||
end
|
||||
|
||||
it 'tracks add suggestion event' do
|
||||
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
|
||||
.to receive(:track_add_suggestion_action)
|
||||
.with(user: note.author)
|
||||
|
||||
subject.execute
|
||||
end
|
||||
|
||||
context 'outdated position note' do
|
||||
let!(:outdated_diff) { merge_request.merge_request_diff }
|
||||
let!(:latest_diff) { merge_request.create_merge_request_diff }
|
||||
|
|
|
|||
|
|
@ -41,5 +41,11 @@ RSpec.shared_examples 'can housekeep repository' do
|
|||
expect(resource.send(:pushes_since_gc_redis_shared_state_key)).to eq("#{resource_key}/#{resource.id}/pushes_since_gc")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#git_garbage_collect_worker_klass' do
|
||||
it 'defines a git gargabe collect worker' do
|
||||
expect(resource.git_garbage_collect_worker_klass).to eq(expected_worker_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@
|
|||
RSpec.shared_examples 'housekeeps repository' do
|
||||
subject { described_class.new(resource) }
|
||||
|
||||
context 'with a clean redis state', :clean_gitlab_redis_shared_state do
|
||||
context 'with a clean redis state', :clean_gitlab_redis_shared_state, :aggregate_failures do
|
||||
describe '#execute' do
|
||||
it 'enqueues a sidekiq job' 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(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
|
||||
expect(resource.git_garbage_collect_worker_klass).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(Projects::GitGarbageCollectWorker.jobs, :size).by(1)
|
||||
expect { subject.execute }.to change(resource.git_garbage_collect_worker_klass.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(Projects::GitGarbageCollectWorker).not_to receive(:perform_async)
|
||||
expect(resource.git_garbage_collect_worker_klass).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(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid)
|
||||
expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid)
|
||||
.once
|
||||
# At push 50, 100, 150
|
||||
expect(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :full_repack, :the_lease_key, :the_uuid)
|
||||
expect(resource.git_garbage_collect_worker_klass).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(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid)
|
||||
expect(resource.git_garbage_collect_worker_klass).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(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid)
|
||||
expect(resource.git_garbage_collect_worker_klass).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(Projects::GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :gc_lease_key, :gc_uuid).twice
|
||||
expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :gc, :gc_lease_key, :gc_uuid).twice
|
||||
|
||||
2.times do
|
||||
housekeeping.execute
|
||||
|
|
|
|||
|
|
@ -0,0 +1,320 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fileutils'
|
||||
|
||||
RSpec.shared_examples 'can collect git garbage' do |update_statistics: true|
|
||||
include GitHelpers
|
||||
|
||||
let!(:lease_uuid) { SecureRandom.uuid }
|
||||
let!(:lease_key) { "resource_housekeeping:#{resource.id}" }
|
||||
let(:params) { [resource.id, task, lease_key, lease_uuid] }
|
||||
let(:shell) { Gitlab::Shell.new }
|
||||
let(:repository) { resource.repository }
|
||||
let(:statistics_service_klass) { nil }
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:find_resource).and_return(resource)
|
||||
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 resource statistics' do
|
||||
it 'updates the resource statistics' do
|
||||
expect_next_instance_of(statistics_service_klass, anything, nil, statistics: statistics_keys) 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(statistics_service_klass).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 resource statistics' if update_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) { [resource.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 resource statistics' if update_statistics
|
||||
|
||||
it "flushes ref caches when the task if 'gc'" do
|
||||
expect(subject).to receive(:get_lease_uuid).with("git_gc:#{task}:#{expected_default_lease}").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
|
||||
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 resource statistics' if update_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 resource statistics' do
|
||||
expect(statistics_service_klass).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 resource statistics' if update_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(resource)
|
||||
before_packs = packs(resource)
|
||||
|
||||
expect(before_packs.count).to be >= 1
|
||||
|
||||
subject.perform(resource.id, 'incremental_repack', lease_key, lease_uuid)
|
||||
after_packs = packs(resource)
|
||||
|
||||
# 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(resource)
|
||||
subject.perform(resource.id, 'incremental_repack', lease_key, lease_uuid)
|
||||
before_packs = packs(resource)
|
||||
|
||||
expect(before_packs.count).to be >= 2
|
||||
|
||||
subject.perform(resource.id, 'full_repack', lease_key, lease_uuid)
|
||||
after_packs = packs(resource)
|
||||
|
||||
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(resource)
|
||||
before_packs = packs(resource)
|
||||
before_packed_refs = packed_refs(resource)
|
||||
|
||||
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(resource.id, 'gc', lease_key, lease_uuid)
|
||||
after_packed_refs = packed_refs(resource)
|
||||
after_packs = packs(resource)
|
||||
|
||||
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(resource).to receive(:cleanup).and_call_original
|
||||
|
||||
subject.perform(resource.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(resource.id, 'prune', lease_key, lease_uuid)
|
||||
end
|
||||
|
||||
# Create a new commit on a random new branch
|
||||
def create_objects(resource)
|
||||
rugged = rugged_repo(resource.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(resource)
|
||||
Dir["#{path_to_repo}/objects/pack/*.pack"]
|
||||
end
|
||||
|
||||
def packed_refs(resource)
|
||||
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, resource.repository.relative_path)
|
||||
end
|
||||
|
||||
def bitmap_path(pack)
|
||||
pack.sub(/\.pack\z/, '.bitmap')
|
||||
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
|
||||
end
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'shared/ssh_keys/_key_delete.html.haml' do
|
||||
context 'when the text parameter is used' do
|
||||
it 'has text' do
|
||||
render 'shared/ssh_keys/key_delete.html.haml', text: 'Button', html_class: '', button_data: ''
|
||||
|
||||
expect(rendered).to have_button('Button')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the text parameter is not used' do
|
||||
it 'does not have text' do
|
||||
render 'shared/ssh_keys/key_delete.html.haml', html_class: '', button_data: ''
|
||||
|
||||
expect(rendered).to have_button('Delete')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,374 +1,78 @@
|
|||
# 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)
|
||||
it_behaves_like 'can collect git garbage' do
|
||||
let(:resource) { project }
|
||||
let(:statistics_service_klass) { Projects::UpdateStatisticsService }
|
||||
let(:statistics_keys) { [:repository_size, :lfs_objects_size] }
|
||||
let(:expected_default_lease) { "#{resource.id}" }
|
||||
end
|
||||
|
||||
shared_examples 'it calls Gitaly' do
|
||||
specify do
|
||||
repository_service = instance_double(Gitlab::GitalyClient::RepositoryService)
|
||||
context 'when is able to get the lease' do
|
||||
let(:params) { [project.id] }
|
||||
|
||||
expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service)
|
||||
expect(repository_service).to receive(gitaly_task)
|
||||
subject { described_class.new }
|
||||
|
||||
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)
|
||||
before do
|
||||
allow(subject).to receive(:get_lease_uuid).and_return(false)
|
||||
allow(subject).to receive(:find_resource).and_return(project)
|
||||
allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
|
||||
end
|
||||
|
||||
it 'does nothing if the database is read-only' do
|
||||
allow(Gitlab::Database).to receive(:read_only?) { true }
|
||||
context 'when the repository has joined a pool' do
|
||||
let!(:pool) { create(:pool_repository, :ready) }
|
||||
let(:project) { pool.source_project }
|
||||
|
||||
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
|
||||
it 'ensures the repositories are linked' do
|
||||
expect(project.pool_repository).to receive(:link_repository).once
|
||||
|
||||
subject.perform(*params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no active lease' do
|
||||
let(:params) { [project.id] }
|
||||
context 'LFS object garbage collection' do
|
||||
let_it_be(:lfs_reference) { create(:lfs_objects_project, project: project) }
|
||||
let(:lfs_object) { lfs_reference.lfs_object }
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:get_lease_uuid).and_return(false)
|
||||
stub_lfs_setting(enabled: true)
|
||||
end
|
||||
|
||||
context 'when is able to get the lease' do
|
||||
before do
|
||||
allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
|
||||
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
|
||||
|
||||
it_behaves_like 'it calls Gitaly'
|
||||
it_behaves_like 'it updates the project statistics'
|
||||
subject.perform(*params)
|
||||
|
||||
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
|
||||
expect(project.lfs_objects.reload).not_to include(lfs_object)
|
||||
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)
|
||||
it 'catches and logs exceptions' do
|
||||
allow_next_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences) do |svc|
|
||||
allow(svg).to receive(:run!).and_raise(/Failed/)
|
||||
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)
|
||||
expect(Gitlab::GitLogger).to receive(:warn)
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
|
||||
subject.perform(*params)
|
||||
end
|
||||
|
||||
it 'does not update the project statistics' do
|
||||
expect(Projects::UpdateStatisticsService).not_to receive(:new)
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Wikis::GitGarbageCollectWorker do
|
||||
it_behaves_like 'can collect git garbage' do
|
||||
let_it_be(:resource) { create(:project_wiki) }
|
||||
let_it_be(:page) { create(:wiki_page, wiki: resource) }
|
||||
|
||||
let(:statistics_service_klass) { Projects::UpdateStatisticsService }
|
||||
let(:statistics_keys) { [:wiki_size] }
|
||||
let(:expected_default_lease) { "project_wikis:#{resource.id}" }
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue