diff options
87 files changed, 1631 insertions, 684 deletions
diff --git a/app/assets/javascripts/jira_connect/api.js b/app/assets/javascripts/jira_connect/api.js index d689a2d1962..9cd85396fbc 100644 --- a/app/assets/javascripts/jira_connect/api.js +++ b/app/assets/javascripts/jira_connect/api.js @@ -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) => { diff --git a/app/assets/javascripts/jira_connect/components/groups_list.vue b/app/assets/javascripts/jira_connect/components/groups_list.vue index eeddd32addc..8671ecaa78a 100644 --- a/app/assets/javascripts/jira_connect/components/groups_list.vue +++ b/app/assets/javascripts/jira_connect/components/groups_list.vue @@ -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> diff --git a/app/assets/javascripts/jira_connect/components/groups_list_item.vue b/app/assets/javascripts/jira_connect/components/groups_list_item.vue index b0df8d03feb..305f440707e 100644 --- a/app/assets/javascripts/jira_connect/components/groups_list_item.vue +++ b/app/assets/javascripts/jira_connect/components/groups_list_item.vue @@ -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> diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js index dc2a77f4e0c..2c77717e2fc 100644 --- a/app/assets/javascripts/jira_connect/index.js +++ b/app/assets/javascripts/jira_connect/index.js @@ -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); diff --git a/app/assets/javascripts/jira_connect/store/index.js b/app/assets/javascripts/jira_connect/store/index.js index aa7e14269a4..de830e3891a 100644 --- a/app/assets/javascripts/jira_connect/store/index.js +++ b/app/assets/javascripts/jira_connect/store/index.js @@ -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, }); diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 79d9ba6df57..5be2b6946e5 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -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" /> diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 3b59c028437..5d182373fb1 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -110,7 +110,7 @@ pre { } hr { - margin: 24px 0; + margin: 1.5rem 0; border-top: 1px solid $gray-darker; } diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb index f1527b9b85a..1f50a7383b3 100644 --- a/app/helpers/jira_connect_helper.rb +++ b/app/helpers/jira_connect_helper.rb @@ -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 diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index d1c0bb11dc8..32c9d44f836 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -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 diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb index 82055822cfb..c7af841e450 100644 --- a/app/models/concerns/optimized_issuable_label_filter.rb +++ b/app/models/concerns/optimized_issuable_label_filter.rb @@ -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) diff --git a/app/models/concerns/repositories/can_housekeep_repository.rb b/app/models/concerns/repositories/can_housekeep_repository.rb index 2b79851a07c..946f82c5f36 100644 --- a/app/models/concerns/repositories/can_housekeep_repository.rb +++ b/app/models/concerns/repositories/can_housekeep_repository.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index df8427481fc..fb27448b6e4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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) diff --git a/app/models/wiki.rb b/app/models/wiki.rb index 11c10a61d18..ab53515ec48 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -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) diff --git a/app/services/repositories/housekeeping_service.rb b/app/services/repositories/housekeeping_service.rb index e97c295e18e..de80390e60b 100644 --- a/app/services/repositories/housekeeping_service.rb +++ b/app/services/repositories/housekeeping_service.rb @@ -45,7 +45,7 @@ module Repositories private def execute_gitlab_shell_gc(lease_uuid) - 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 diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb index ab80b23a37b..0ce3fbe115f 100644 --- a/app/services/suggestions/apply_service.rb +++ b/app/services/suggestions/apply_service.rb @@ -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 diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb index 93d2bd11426..a97c36fa0ca 100644 --- a/app/services/suggestions/create_service.rb +++ b/app/services/suggestions/create_service.rb @@ -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 diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml index ed765f80b74..b95bc729e79 100644 --- a/app/views/jira_connect/subscriptions/index.html.haml +++ b/app/views/jira_connect/subscriptions/index.html.haml @@ -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 diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index eaf00ce6709..38fea521578 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -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)) diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 22d795ca831..8016d989ff1 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -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)) diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml index 9415516d6f6..6e46423cde0 100644 --- a/app/views/projects/runners/_group_runners.html.haml +++ b/app/views/projects/runners/_group_runners.html.haml @@ -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') diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 85bd0335b92..41159df1435 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -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} diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index fd8b4eb0d39..484d8f8a40c 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -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 diff --git a/app/views/shared/ssh_keys/_key_delete.html.haml b/app/views/shared/ssh_keys/_key_delete.html.haml new file mode 100644 index 00000000000..1526e5d3eda --- /dev/null +++ b/app/views/shared/ssh_keys/_key_delete.html.haml @@ -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') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 6179265149e..9b6c9625e28 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -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: diff --git a/app/workers/concerns/git_garbage_collect_methods.rb b/app/workers/concerns/git_garbage_collect_methods.rb new file mode 100644 index 00000000000..17a80d1ddb3 --- /dev/null +++ b/app/workers/concerns/git_garbage_collect_methods.rb @@ -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 diff --git a/app/workers/projects/git_garbage_collect_worker.rb b/app/workers/projects/git_garbage_collect_worker.rb index aba99ce35d0..7c7c7f27a6b 100644 --- a/app/workers/projects/git_garbage_collect_worker.rb +++ b/app/workers/projects/git_garbage_collect_worker.rb @@ -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 - end + override :before_gitaly_call + def before_gitaly_call(task, resource) + return unless gc?(task) - def renew_lease(key, uuid) - ::Gitlab::ExclusiveLease.new(key, uuid: uuid, timeout: LEASE_TIMEOUT).renew + ::Projects::GitDeduplicationService.new(resource).execute + cleanup_orphan_lfs_file_references(resource) 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 diff --git a/app/workers/wikis/git_garbage_collect_worker.rb b/app/workers/wikis/git_garbage_collect_worker.rb new file mode 100644 index 00000000000..1b455c50618 --- /dev/null +++ b/app/workers/wikis/git_garbage_collect_worker.rb @@ -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 diff --git a/changelogs/unreleased/223101-replace-angle-double-left-icon-with-svg.yml b/changelogs/unreleased/223101-replace-angle-double-left-icon-with-svg.yml new file mode 100644 index 00000000000..bc759b102b7 --- /dev/null +++ b/changelogs/unreleased/223101-replace-angle-double-left-icon-with-svg.yml @@ -0,0 +1,5 @@ +--- +title: Replace angle-double-left icon with chevron-double-lg-left +merge_request: 52393 +author: +type: other diff --git a/changelogs/unreleased/231044-convert-runner-buttons-to-pajamas.yml b/changelogs/unreleased/231044-convert-runner-buttons-to-pajamas.yml new file mode 100644 index 00000000000..3902f543bf8 --- /dev/null +++ b/changelogs/unreleased/231044-convert-runner-buttons-to-pajamas.yml @@ -0,0 +1,5 @@ +--- +title: Convert project runner buttons to pajamas +merge_request: 52358 +author: +type: other diff --git a/changelogs/unreleased/259719-rollout-optimized-labels-ff.yml b/changelogs/unreleased/259719-rollout-optimized-labels-ff.yml new file mode 100644 index 00000000000..5e0bc464762 --- /dev/null +++ b/changelogs/unreleased/259719-rollout-optimized-labels-ff.yml @@ -0,0 +1,5 @@ +--- +title: Improve the performance of merge request and issue search by label(s) +merge_request: 52495 +author: +type: performance diff --git a/changelogs/unreleased/267522-sshkey-delete-parital.yml b/changelogs/unreleased/267522-sshkey-delete-parital.yml new file mode 100644 index 00000000000..2c27cbe551a --- /dev/null +++ b/changelogs/unreleased/267522-sshkey-delete-parital.yml @@ -0,0 +1,5 @@ +--- +title: New Shared Partial for SSH Key Deletion +merge_request: 50825 +author: Mehul Sharma +type: other diff --git a/changelogs/unreleased/288822-audit-event-removed-user-bug.yml b/changelogs/unreleased/288822-audit-event-removed-user-bug.yml new file mode 100644 index 00000000000..23409642246 --- /dev/null +++ b/changelogs/unreleased/288822-audit-event-removed-user-bug.yml @@ -0,0 +1,5 @@ +--- +title: Fix batch query issue when primary key is -1 +merge_request: 51716 +author: +type: fixed diff --git a/changelogs/unreleased/292828-track-suggestion-metrics.yml b/changelogs/unreleased/292828-track-suggestion-metrics.yml new file mode 100644 index 00000000000..4d6e87baedf --- /dev/null +++ b/changelogs/unreleased/292828-track-suggestion-metrics.yml @@ -0,0 +1,5 @@ +--- +title: Track suggestion add/apply metrics +merge_request: 52189 +author: +type: other diff --git a/changelogs/unreleased/299052-fj-add-repository-read-only-column-to-namespace-settings.yml b/changelogs/unreleased/299052-fj-add-repository-read-only-column-to-namespace-settings.yml new file mode 100644 index 00000000000..3838938be30 --- /dev/null +++ b/changelogs/unreleased/299052-fj-add-repository-read-only-column-to-namespace-settings.yml @@ -0,0 +1,5 @@ +--- +title: Add repository_read_only column to NamespaceSettings table +merge_request: 52300 +author: +type: added diff --git a/changelogs/unreleased/automatically-authenticate-with-the-dependency-proxy.yml b/changelogs/unreleased/automatically-authenticate-with-the-dependency-proxy.yml new file mode 100644 index 00000000000..38dead31211 --- /dev/null +++ b/changelogs/unreleased/automatically-authenticate-with-the-dependency-proxy.yml @@ -0,0 +1,5 @@ +--- +title: Pass dependency proxy credentials to runners to log in automatically +merge_request: 51927 +author: +type: added diff --git a/changelogs/unreleased/yo-gl-button-mirror-update.yml b/changelogs/unreleased/yo-gl-button-mirror-update.yml new file mode 100644 index 00000000000..a0d2f27f122 --- /dev/null +++ b/changelogs/unreleased/yo-gl-button-mirror-update.yml @@ -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 diff --git a/config/feature_flags/development/ci_instance_variables_ui.yml b/config/feature_flags/development/ci_instance_variables_ui.yml index f5cd2d21bd1..73bc0346818 100644 --- a/config/feature_flags/development/ci_instance_variables_ui.yml +++ b/config/feature_flags/development/ci_instance_variables_ui.yml @@ -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 diff --git a/config/feature_flags/development/ci_store_pipeline_messages.yml b/config/feature_flags/development/ci_store_pipeline_messages.yml index ae20b11f79c..702e4d891a9 100644 --- a/config/feature_flags/development/ci_store_pipeline_messages.yml +++ b/config/feature_flags/development/ci_store_pipeline_messages.yml @@ -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 diff --git a/config/feature_flags/development/optimized_issuable_label_filter.yml b/config/feature_flags/development/optimized_issuable_label_filter.yml index 4712cdaf230..343f40074f0 100644 --- a/config/feature_flags/development/optimized_issuable_label_filter.yml +++ b/config/feature_flags/development/optimized_issuable_label_filter.yml @@ -5,4 +5,4 @@ rollout_issue_url: milestone: '13.4' type: development group: group::analytics -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/usage_data_i_code_review_user_add_suggestion.yml b/config/feature_flags/development/usage_data_i_code_review_user_add_suggestion.yml new file mode 100644 index 00000000000..832be26d50d --- /dev/null +++ b/config/feature_flags/development/usage_data_i_code_review_user_add_suggestion.yml @@ -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 diff --git a/config/feature_flags/development/usage_data_i_code_review_user_apply_suggestion.yml b/config/feature_flags/development/usage_data_i_code_review_user_apply_suggestion.yml new file mode 100644 index 00000000000..03aa1691825 --- /dev/null +++ b/config/feature_flags/development/usage_data_i_code_review_user_apply_suggestion.yml @@ -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 diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index b01239d6aaf..18398cfcbe9 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -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 diff --git a/db/migrate/20210122073805_add_repository_read_only_to_namespace_settings.rb b/db/migrate/20210122073805_add_repository_read_only_to_namespace_settings.rb new file mode 100644 index 00000000000..f6479bdb3a4 --- /dev/null +++ b/db/migrate/20210122073805_add_repository_read_only_to_namespace_settings.rb @@ -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 diff --git a/db/schema_migrations/20210122073805 b/db/schema_migrations/20210122073805 new file mode 100644 index 00000000000..322c90eb820 --- /dev/null +++ b/db/schema_migrations/20210122073805 @@ -0,0 +1 @@ +f5231b1eec17ea1a67f2d2f4ca759314afb85b2c8fb431e3303d530d44bdb1ef
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 2ba8b8a9ff1..5662bba5fe4 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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)) ); diff --git a/doc/development/agent/repository_overview.md b/doc/development/agent/repository_overview.md index 67861425d78..2e250a37e5f 100644 --- a/doc/development/agent/repository_overview.md +++ b/doc/development/agent/repository_overview.md @@ -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. diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md new file mode 100644 index 00000000000..f3741c02cd0 --- /dev/null +++ b/doc/development/usage_ping/dictionary.md @@ -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 | diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md index d86496e75eb..95c1c76806d 100644 --- a/doc/operations/incident_management/incidents.md +++ b/doc/operations/incident_management/incidents.md @@ -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. diff --git a/doc/operations/metrics/alerts.md b/doc/operations/metrics/alerts.md index f8f331c887b..fbc616e7e85 100644 --- a/doc/operations/metrics/alerts.md +++ b/doc/operations/metrics/alerts.md @@ -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 diff --git a/doc/user/application_security/coverage_fuzzing/index.md b/doc/user/application_security/coverage_fuzzing/index.md index 469945246c1..d834fc1cd52 100644 --- a/doc/user/application_security/coverage_fuzzing/index.md +++ b/doc/user/application_security/coverage_fuzzing/index.md @@ -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) diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index ea7629d1f10..4f3a04af8a7 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -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. diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md index 5e5aadfae2b..9d5945786ff 100644 --- a/doc/user/packages/dependency_proxy/index.md +++ b/doc/user/packages/dependency_proxy/index.md @@ -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: diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md index b813e4f7d28..5efb8d9ff40 100644 --- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md +++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md @@ -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 diff --git a/lib/gitlab/ci/build/credentials/base.rb b/lib/gitlab/ci/build/credentials/base.rb index 58adf6e506d..2aeb8453703 100644 --- a/lib/gitlab/ci/build/credentials/base.rb +++ b/lib/gitlab/ci/build/credentials/base.rb @@ -6,7 +6,7 @@ module Gitlab module Credentials class Base def type - self.class.name.demodulize.underscore + raise NotImplementedError end end end diff --git a/lib/gitlab/ci/build/credentials/factory.rb b/lib/gitlab/ci/build/credentials/factory.rb index fa805abb8bb..e8996cb9dc4 100644 --- a/lib/gitlab/ci/build/credentials/factory.rb +++ b/lib/gitlab/ci/build/credentials/factory.rb @@ -20,7 +20,7 @@ module Gitlab end def providers - [Registry] + [Registry::GitlabRegistry, Registry::DependencyProxy] end end end diff --git a/lib/gitlab/ci/build/credentials/registry.rb b/lib/gitlab/ci/build/credentials/registry.rb deleted file mode 100644 index 1c8588d9913..00000000000 --- a/lib/gitlab/ci/build/credentials/registry.rb +++ /dev/null @@ -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 diff --git a/lib/gitlab/ci/build/credentials/registry/dependency_proxy.rb b/lib/gitlab/ci/build/credentials/registry/dependency_proxy.rb new file mode 100644 index 00000000000..b6ac06cfb53 --- /dev/null +++ b/lib/gitlab/ci/build/credentials/registry/dependency_proxy.rb @@ -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 diff --git a/lib/gitlab/ci/build/credentials/registry/gitlab_registry.rb b/lib/gitlab/ci/build/credentials/registry/gitlab_registry.rb new file mode 100644 index 00000000000..5bd30e677e9 --- /dev/null +++ b/lib/gitlab/ci/build/credentials/registry/gitlab_registry.rb @@ -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 diff --git a/lib/gitlab/usage/docs/helper.rb b/lib/gitlab/usage/docs/helper.rb new file mode 100644 index 00000000000..1ed85912dba --- /dev/null +++ b/lib/gitlab/usage/docs/helper.rb @@ -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 diff --git a/lib/gitlab/usage/docs/renderer.rb b/lib/gitlab/usage/docs/renderer.rb new file mode 100644 index 00000000000..7a7c58005bb --- /dev/null +++ b/lib/gitlab/usage/docs/renderer.rb @@ -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 diff --git a/lib/gitlab/usage/docs/templates/default.md.haml b/lib/gitlab/usage/docs/templates/default.md.haml new file mode 100644 index 00000000000..86e93be66c7 --- /dev/null +++ b/lib/gitlab/usage/docs/templates/default.md.haml @@ -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) diff --git a/lib/gitlab/usage/docs/value_formatter.rb b/lib/gitlab/usage/docs/value_formatter.rb new file mode 100644 index 00000000000..37db377ccba --- /dev/null +++ b/lib/gitlab/usage/docs/value_formatter.rb @@ -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 diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml index 4cbde0c0372..7eab1818392 100644 --- a/lib/gitlab/usage_data_counters/known_events/common.yml +++ b/lib/gitlab/usage_data_counters/known_events/common.yml @@ -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 diff --git a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb index 11d59257ed9..78b04a89aed 100644 --- a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb +++ b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb @@ -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) diff --git a/lib/tasks/gitlab/usage_data.rake b/lib/tasks/gitlab/usage_data.rake index d6f5661d5eb..0e729fa8833 100644 --- a/lib/tasks/gitlab/usage_data.rake +++ b/lib/tasks/gitlab/usage_data.rake @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fd23f9b9cfe..9bc44487d76 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/factories/audit_events.rb b/spec/factories/audit_events.rb index 4e72976a9e5..05b86d2f13b 100644 --- a/spec/factories/audit_events.rb +++ b/spec/factories/audit_events.rb @@ -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) } } diff --git a/spec/frontend/jira_connect/api_spec.js b/spec/frontend/jira_connect/api_spec.js index 8fecbee9ca7..f98d56a6621 100644 --- a/spec/frontend/jira_connect/api_spec.js +++ b/spec/frontend/jira_connect/api_spec.js @@ -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: { diff --git a/spec/frontend/jira_connect/components/groups_list_item_spec.js b/spec/frontend/jira_connect/components/groups_list_item_spec.js index 77577c53cf4..d7aef6932f4 100644 --- a/spec/frontend/jira_connect/components/groups_list_item_spec.js +++ b/spec/frontend/jira_connect/components/groups_list_item_spec.js @@ -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 reloadSpy = jest.fn(); + + global.AP = { + navigator: { + reload: reloadSpy, + }, + }; - const createComponent = () => { + 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 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 description', () => { - expect(findGroupDescription().text()).toBe(mockGroup1.description); + describe('on Link button click', () => { + let addSubscriptionSpy; + + 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); + }); + }); }); }); diff --git a/spec/frontend/jira_connect/components/groups_list_spec.js b/spec/frontend/jira_connect/components/groups_list_spec.js index 94f158e6344..b18a913ca6a 100644 --- a/spec/frontend/jira_connect/components/groups_list_spec.js +++ b/spec/frontend/jira_connect/components/groups_list_spec.js @@ -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); + }); }); }); diff --git a/spec/frontend/jira_connect/mock_data.js b/spec/frontend/jira_connect/mock_data.js index 31565912489..22255fabc3d 100644 --- a/spec/frontend/jira_connect/mock_data.js +++ b/spec/frontend/jira_connect/mock_data.js @@ -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', }; diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb index a99072527c8..b9b6e1370e8 100644 --- a/spec/helpers/jira_connect_helper_spec.rb +++ b/spec/helpers/jira_connect_helper_spec.rb @@ -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 diff --git a/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb new file mode 100644 index 00000000000..f50c6e99e99 --- /dev/null +++ b/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb @@ -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 diff --git a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb index c0a76973f60..43913e91085 100644 --- a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb +++ b/spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb @@ -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' } diff --git a/spec/lib/gitlab/usage/docs/renderer_spec.rb b/spec/lib/gitlab/usage/docs/renderer_spec.rb new file mode 100644 index 00000000000..e62861cd677 --- /dev/null +++ b/spec/lib/gitlab/usage/docs/renderer_spec.rb @@ -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 diff --git a/spec/lib/gitlab/usage/docs/value_formatter_spec.rb b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb new file mode 100644 index 00000000000..ceb00867c95 --- /dev/null +++ b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb @@ -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 diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb index c7b208cfb31..b4c230e77e7 100644 --- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb @@ -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 diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 49417a7ae5c..67a80924cbc 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -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 diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 8001d009901..c04fc70deca 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -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 diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index 3e7594bd30f..e31dd7031eb 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -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 + 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 + end + end + context 'diff file was not found' do - it 'returns error message' do - expect(suggestion.note).to receive(:latest_diff_file) { nil } + let(:result) { apply_service.new(user, suggestion).execute } - 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 - 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) + let(:merge_request2) do + create( + :merge_request, + :conflict, + source_project: project, + target_project: project + ) + end - diff_note2 = create(:diff_note_on_merge_request, - noteable: merge_request2, - position: position2, - project: project) + 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 - other_branch_suggestion = create(:suggestion, note: diff_note2) + let(:diff_note2) do + create( + :diff_note_on_merge_request, + noteable: merge_request2, + position: position2, + project: project + ) + end - result = apply_service.new(user, suggestion, other_branch_suggestion).execute + 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 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 diff --git a/spec/services/suggestions/create_service_spec.rb b/spec/services/suggestions/create_service_spec.rb index 80823364fe8..5148d6756fc 100644 --- a/spec/services/suggestions/create_service_spec.rb +++ b/spec/services/suggestions/create_service_spec.rb @@ -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 } diff --git a/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb index 2f0b95427d2..4006b8226ce 100644 --- a/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb index ad0dc389aeb..4c00faee56b 100644 --- a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb +++ b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb new file mode 100644 index 00000000000..f2314793cb4 --- /dev/null +++ b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb @@ -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 diff --git a/spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb b/spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb new file mode 100644 index 00000000000..400319a42b7 --- /dev/null +++ b/spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb @@ -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 diff --git a/spec/workers/projects/git_garbage_collect_worker_spec.rb b/spec/workers/projects/git_garbage_collect_worker_spec.rb index 9ef1cce117e..73a70ba436b 100644 --- a/spec/workers/projects/git_garbage_collect_worker_spec.rb +++ b/spec/workers/projects/git_garbage_collect_worker_spec.rb @@ -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) - end - - shared_examples 'it calls Gitaly' do - specify do - repository_service = instance_double(Gitlab::GitalyClient::RepositoryService) - - expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service) - expect(repository_service).to receive(gitaly_task) - - subject.perform(*params) - end - end - - shared_examples 'it updates the project statistics' do - it 'updates the project statistics' do - expect_next_instance_of(Projects::UpdateStatisticsService, project, nil, statistics: [:repository_size, :lfs_objects_size]) do |service| - expect(service).to receive(:execute) - end - - subject.perform(*params) - end - - it 'does nothing if the database is read-only' do - allow(Gitlab::Database).to receive(:read_only?) { true } - - expect(Projects::UpdateStatisticsService).not_to receive(:new) - - subject.perform(*params) - end + 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 - 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' + context 'when is able to get the lease' do + let(:params) { [project.id] } - 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 { described_class.new } - 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 + 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 - context 'with different lease than the active one' do - before do - allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid) - end + context 'when the repository has joined a pool' do + let!(:pool) { create(:pool_repository, :ready) } + let(:project) { pool.source_project } - 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' - - 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) + 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) - 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) + expect(project.lfs_objects.reload).to include(lfs_object) 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 diff --git a/spec/workers/wikis/git_garbage_collect_worker_spec.rb b/spec/workers/wikis/git_garbage_collect_worker_spec.rb new file mode 100644 index 00000000000..77c2e49a83a --- /dev/null +++ b/spec/workers/wikis/git_garbage_collect_worker_spec.rb @@ -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 |