Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-25 18:09:03 +00:00
parent 10cc2d7a72
commit 899bb5c4a9
87 changed files with 1640 additions and 693 deletions

View File

@ -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) => {

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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,
});

View File

@ -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"
/>

View File

@ -110,7 +110,7 @@ pre {
}
hr {
margin: 24px 0;
margin: 1.5rem 0;
border-top: 1px solid $gray-darker;
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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))

View File

@ -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')
&nbsp;
= _('for this project')

View File

@ -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}

View File

@ -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')
&nbsp; for this project

View File

@ -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')

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Replace angle-double-left icon with chevron-double-lg-left
merge_request: 52393
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Convert project runner buttons to pajamas
merge_request: 52358
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Improve the performance of merge request and issue search by label(s)
merge_request: 52495
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: New Shared Partial for SSH Key Deletion
merge_request: 50825
author: Mehul Sharma
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix batch query issue when primary key is -1
merge_request: 51716
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Track suggestion add/apply metrics
merge_request: 52189
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Add repository_read_only column to NamespaceSettings table
merge_request: 52300
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Pass dependency proxy credentials to runners to log in automatically
merge_request: 51927
author:
type: added

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -5,4 +5,4 @@ rollout_issue_url:
milestone: '13.4'
type: development
group: group::analytics
default_enabled: false
default_enabled: true

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
f5231b1eec17ea1a67f2d2f4ca759314afb85b2c8fb431e3303d530d44bdb1ef

View File

@ -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))
);

View File

@ -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.

View File

@ -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 |

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -6,7 +6,7 @@ module Gitlab
module Credentials
class Base
def type
self.class.name.demodulize.underscore
raise NotImplementedError
end
end
end

View File

@ -20,7 +20,7 @@ module Gitlab
end
def providers
[Registry]
[Registry::GitlabRegistry, Registry::DependencyProxy]
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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 ""

View File

@ -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) } }

View File

@ -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: {

View File

@ -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);
});
});
});
});

View File

@ -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);
});
});
});

View File

@ -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',
};

View File

@ -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

View File

@ -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

View File

@ -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' }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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