Commit 561e1b47 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 7b52c7cb
......@@ -476,12 +476,12 @@ const Api = {
return axios.get(url);
},
lsifData(projectPath, commitId, path) {
lsifData(projectPath, commitId, paths) {
const url = Api.buildUrl(this.lsifPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':commit_id', commitId);
return axios.get(url, { params: { path } });
return axios.get(url, { params: { paths } });
},
environments(id) {
......
......@@ -13,9 +13,10 @@ export default {
commit(types.REQUEST_DATA);
api
.lsifData(state.projectPath, state.commitId, state.blobPath)
.lsifData(state.projectPath, state.commitId, [state.blobPath])
.then(({ data }) => {
const normalizedData = data.reduce((acc, d) => {
const dataForPath = data[state.blobPath];
const normalizedData = dataForPath.reduce((acc, d) => {
if (d.hover) {
acc[`${d.start_line}:${d.start_char}`] = d;
addInteractionClass(d);
......
......@@ -38,7 +38,7 @@ module CycleAnalyticsParams
end
def to_utc_time(field)
date = field.is_a?(Date) ? field : Date.parse(field)
date = field.is_a?(Date) || field.is_a?(Time) ? field : Date.parse(field)
date.to_time.utc
end
end
......
......@@ -10,6 +10,11 @@ module Resolvers
def resolve(**args)
return Snippet.none if project.nil?
unless project.feature_available?(:snippets, current_user)
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
'Snippets are not enabled for this Project'
end
super
end
......
......@@ -2039,6 +2039,16 @@ class Project < ApplicationRecord
end
end
def change_repository_storage(new_repository_storage_key)
return if repository_read_only?
return if repository_storage == new_repository_storage_key
raise ArgumentError unless ::Gitlab.config.repositories.storages.key?(new_repository_storage_key)
run_after_commit { ProjectUpdateRepositoryStorageWorker.perform_async(id, new_repository_storage_key) }
self.repository_read_only = true
end
def pushes_since_gc
Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
end
......
......@@ -469,6 +469,8 @@ class ProjectPolicy < BasePolicy
prevent :create_pipeline
end
rule { admin }.enable :change_repository_storage
private
def team_member?
......
......@@ -53,7 +53,7 @@ module Projects
#### Summary
#{metadata_list}
#{alert_details}
#{alert_details}#{metric_embed_for_alert}
MARKDOWN
end
......@@ -118,6 +118,10 @@ module Projects
def host_links
Array(hosts.value).join(' ')
end
def metric_embed_for_alert; end
end
end
end
Projects::Prometheus::AlertPresenter.prepend_if_ee('EE::Projects::Prometheus::AlertPresenter')
......@@ -3,18 +3,15 @@
class SnippetBlobPresenter < BlobPresenter
def rich_data
return if blob.binary?
return unless blob.rich_viewer
if markup?
blob.rendered_markup
else
highlight(plain: false)
end
render_rich_partial
end
def plain_data
return if blob.binary?
highlight(plain: !markup?)
highlight(plain: false)
end
def raw_path
......@@ -27,10 +24,6 @@ class SnippetBlobPresenter < BlobPresenter
private
def markup?
blob.rich_viewer&.partial_name == 'markup'
end
def snippet
blob.container
end
......@@ -38,4 +31,18 @@ class SnippetBlobPresenter < BlobPresenter
def language
nil
end
def render_rich_partial
renderer.render("projects/blob/viewers/_#{blob.rich_viewer.partial_name}",
locals: { viewer: blob.rich_viewer, blob: blob, blob_raw_path: raw_path },
layout: false)
end
def renderer
proxy = Warden::Proxy.new({}, Warden::Manager.new({})).tap do |proxy_instance|
proxy_instance.set_user(current_user, scope: :user)
end
ApplicationController.renderer.new('warden' => proxy)
end
end
......@@ -61,10 +61,15 @@ module Projects
end
def filter_by_name(tags)
regex = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex']}\\z")
# Technical Debt: https://gitlab.com/gitlab-org/gitlab/issues/207267
# name_regex to be removed when container_expiration_policies is updated
# to have both regex columns
regex_delete = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_delete'] || params['name_regex']}\\z")
regex_retain = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_keep']}\\z")
tags.select do |tag|
regex.scan(tag.name).any?
# regex_retain will override any overlapping matches by regex_delete
regex_delete.match?(tag.name) && !regex_retain.match?(tag.name)
end
end
......
......@@ -2,20 +2,22 @@
module Projects
class LsifDataService
attr_reader :file, :project, :path, :commit_id,
:docs, :doc_ranges, :ranges, :def_refs, :hover_refs
attr_reader :file, :project, :commit_id, :docs,
:doc_ranges, :ranges, :def_refs, :hover_refs
CACHE_EXPIRE_IN = 1.hour
def initialize(file, project, params)
def initialize(file, project, commit_id)
@file = file
@project = project
@path = params[:path]
@commit_id = params[:commit_id]
end
@commit_id = commit_id
def execute
fetch_data!
end
def execute(path)
doc_id = find_doc_id(docs, path)
dir_absolute_path = docs[doc_id]&.delete_suffix(path)
doc_ranges[doc_id]&.map do |range_id|
location, ref_id = ranges[range_id].values_at('loc', 'ref_id')
......@@ -26,7 +28,7 @@ module Projects
end_line: line_data.last,
start_char: column_data.first,
end_char: column_data.last,
definition_url: definition_url_for(def_refs[ref_id]),
definition_url: definition_url_for(def_refs[ref_id], dir_absolute_path),
hover: highlighted_hover(hover_refs[ref_id])
}
end
......@@ -58,8 +60,8 @@ module Projects
@hover_refs = data['hover_refs']
end
def doc_id
@doc_id ||= docs.reduce(nil) do |doc_id, (id, doc_path)|
def find_doc_id(docs, path)
docs.reduce(nil) do |doc_id, (id, doc_path)|
next doc_id unless doc_path =~ /#{path}$/
if doc_id.nil? || docs[doc_id].size > doc_path.size
......@@ -70,11 +72,7 @@ module Projects
end
end
def dir_absolute_path
@dir_absolute_path ||= docs[doc_id]&.delete_suffix(path)
end
def definition_url_for(ref_id)
def definition_url_for(ref_id, dir_absolute_path)
return unless range = ranges[ref_id]
def_doc_id, location = range.values_at('doc_id', 'loc')
......
# frozen_string_literal: true
module Projects
class UpdateRepositoryStorageService < BaseService
include Gitlab::ShellAdapter
RepositoryAlreadyMoved = Class.new(StandardError)
def initialize(project)
@project = project
end
def execute(new_repository_storage_key)
# Raising an exception is a little heavy handed but this behavior (doing
# nothing if the repo is already on the right storage) prevents data
# loss, so it is valuable for us to be able to observe it via the
# exception.
raise RepositoryAlreadyMoved if project.repository_storage == new_repository_storage_key
if mirror_repositories(new_repository_storage_key)
mark_old_paths_for_archive
project.update(repository_storage: new_repository_storage_key, repository_read_only: false)
project.leave_pool_repository
project.track_project_repository
enqueue_housekeeping
else
project.update(repository_read_only: false)
end
end
private
def mirror_repositories(new_repository_storage_key)
result = mirror_repository(new_repository_storage_key)
if project.wiki.repository_exists?
result &&= mirror_repository(new_repository_storage_key, type: Gitlab::GlRepository::WIKI)
end
result
end
def mirror_repository(new_storage_key, type: Gitlab::GlRepository::PROJECT)
return false unless wait_for_pushes(type)
repository = type.repository_for(project)
full_path = repository.full_path
raw_repository = repository.raw
# Initialize a git repository on the target path
gitlab_shell.create_repository(new_storage_key, raw_repository.relative_path, full_path)
new_repository = Gitlab::Git::Repository.new(new_storage_key,
raw_repository.relative_path,
raw_repository.gl_repository,
full_path)
new_repository.fetch_repository_as_mirror(raw_repository)
end
def mark_old_paths_for_archive
old_repository_storage = project.repository_storage
new_project_path = moved_path(project.disk_path)
# Notice that the block passed to `run_after_commit` will run with `project`
# as its context
project.run_after_commit do
GitlabShellWorker.perform_async(:mv_repository,
old_repository_storage,
disk_path,
new_project_path)
if wiki.repository_exists?
GitlabShellWorker.perform_async(:mv_repository,
old_repository_storage,
wiki.disk_path,
"#{new_project_path}.wiki")
end
end
end
def moved_path(path)
"#{path}+#{project.id}+moved+#{Time.now.to_i}"
end
# The underlying FetchInternalRemote call uses a `git fetch` to move data
# to the new repository, which leaves it in a less-well-packed state,
# lacking bitmaps and commit graphs. Housekeeping will boost performance
# significantly.
def enqueue_housekeeping
return unless Gitlab::CurrentSettings.housekeeping_enabled?
return unless Feature.enabled?(:repack_after_shard_migration, project)
Projects::HousekeepingService.new(project, :gc).execute
rescue Projects::HousekeepingService::LeaseTaken
# No action required
end
def wait_for_pushes(type)
reference_counter = project.reference_counter(type: type)
# Try for 30 seconds, polling every 10
3.times do
return true if reference_counter.value == 0
sleep 10
end
false
end
end
end
Projects::UpdateRepositoryStorageService.prepend_if_ee('EE::Projects::UpdateRepositoryStorageService')
......@@ -13,6 +13,10 @@ module Projects
ensure_wiki_exists if enabling_wiki?
if changing_storage_size?
project.change_repository_storage(params.delete(:repository_storage))
end
yield if block_given?
validate_classification_label(project, :external_authorization_classification_label)
......@@ -140,6 +144,13 @@ module Projects
def changing_pages_https_only?
project.previous_changes.include?(:pages_https_only)
end
def changing_storage_size?
new_repository_storage = params[:repository_storage]
new_repository_storage && project.repository.exists? &&
can?(current_user, :change_repository_storage, project)
end
end
end
......
......@@ -1151,6 +1151,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: project_update_repository_storage
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :default
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: propagate_service_template
:feature_category: :source_code_management
:has_external_dependencies:
......
# frozen_string_literal: true
class ProjectUpdateRepositoryStorageWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
feature_category :source_code_management
def perform(project_id, new_repository_storage_key)
project = Project.find(project_id)
::Projects::UpdateRepositoryStorageService.new(project).execute(new_repository_storage_key)
rescue ::Projects::UpdateRepositoryStorageService::RepositoryAlreadyMoved
Rails.logger.info "#{self.class}: repository already moved: #{project}" # rubocop:disable Gitlab/RailsLogger
end
end
---
title: Add name_regex_keep param to container registry bulk delete API endpoint
merge_request: 25484
author:
type: added
---
title: Project Snippets GraphQL resolver checks feature status
merge_request: 26158
author:
type: performance
---
title: Fix snippet blob viewers for rich and plain data
merge_request: 25945
author:
type: fixed
---
title: "Backport API support to move between repository storages/shards"
merge_request: 18721
author: Ben Bodenmiller
type: added
---
title: Automatically include embedded metrics for GitLab alert incidents
merge_request: 25277
author:
type: added
......@@ -231,7 +231,9 @@ DELETE /projects/:id/registry/repositories/:repository_id/tags
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `repository_id` | integer | yes | The ID of registry repository. |
| `name_regex` | string | yes | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to delete. To delete all tags specify `.*`.|
| `name_regex` | string | no | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to delete. To delete all tags specify `.*`. **Note:** `name_regex` is deprecated in favor of `name_regex_delete`.|
| `name_regex_delete` | string | yes | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to delete. To delete all tags specify `.*`.|
| `name_regex_keep` | string | no | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to keep. This value will override any matches from `name_regex_delete`. Note: setting to `.*` will result in a no-op. |
| `keep_n` | integer | no | The amount of latest tags of given name to keep. |
| `older_than` | string | no | Tags to delete that are older than the given time, written in human readable form `1h`, `1d`, `1month`. |
......@@ -239,7 +241,7 @@ This API call performs the following operations:
1. It orders all tags by creation date. The creation date is the time of the
manifest creation, not the time of tag push.
1. It removes only the tags matching the given `name_regex`.
1. It removes only the tags matching the given `name_regex_delete` (or deprecated `name_regex`), keeping any that match `name_regex_keep`.
1. It never removes the tag named `latest`.
1. It keeps N latest matching tags (if `keep_n` is specified).
1. It only removes tags that are older than X amount of time (if `older_than` is specified).
......@@ -261,17 +263,23 @@ Examples:
and remove ones that are older than 2 days:
```shell
curl --request DELETE --data 'name_regex=[0-9a-z]{40}' --data 'keep_n=5' --data 'older_than=2d' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
curl --request DELETE --data 'name_regex_delete=[0-9a-z]{40}' --data 'keep_n=5' --data 'older_than=2d' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
```
1. Remove all tags, but keep always the latest 5:
```shell
curl --request DELETE --data 'name_regex=.*' --data 'keep_n=5' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
curl --request DELETE --data 'name_regex_delete=.*' --data 'keep_n=5' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
```
1. Remove all tags, but keep always tags beginning with `stable`:
```shell
curl --request DELETE --data 'name_regex_delete=.*' --data 'name_regex_keep=stable.*' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
```
1. Remove all tags that are older than 1 month:
```shell
curl --request DELETE --data 'name_regex=.*' --data 'older_than=1month' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
curl --request DELETE --data 'name_regex_delete=.*' --data 'older_than=1month' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
```
......@@ -1041,7 +1041,7 @@ POST /projects
| `ci_config_path` | string | no | The path to CI config file |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project |
| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) |
| `repository_storage` | string | no | **(STARTER ONLY)** Which storage shard the repository is on. Available only to admins |
| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge requests by default |
| `external_authorization_classification_label` | string | no | **(PREMIUM)** The classification label for the project |
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
......@@ -1109,7 +1109,7 @@ POST /projects/user/:user_id
| `ci_config_path` | string | no | The path to CI config file |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project |
| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) |
| `repository_storage` | string | no | **(STARTER ONLY)** Which storage shard the repository is on. Available only to admins |
| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge requests by default |
| `external_authorization_classification_label` | string | no | **(PREMIUM)** The classification label for the project |
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
......@@ -1177,7 +1177,7 @@ PUT /projects/:id
| `ci_default_git_depth` | integer | no | Default number of revisions for [shallow cloning](../user/project/pipelines/settings.md#git-shallow-clone) |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project |
| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) |
| `repository_storage` | string | no | **(STARTER ONLY)** Which storage shard the repository is on. Available only to admins |
| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge request by default |
| `external_authorization_classification_label` | string | no | **(PREMIUM)** The classification label for the project |
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
......
......@@ -136,3 +136,5 @@ Incident Management features can be easily enabled & disabled via the Project se
#### Auto-creation
GitLab Issues can automatically be created as a result of an Alert notification. An Issue created this way will contain error information to help you further debug the error.
For [GitLab-managed alerting rules](../project/integrations/prometheus.md#setting-up-alerts-for-prometheus-metrics-ultimate), the issue will include an embedded chart for the query corresponding to the alert. The chart will show an hour of data surrounding the starting point of the incident, 30 minutes before and after.
......@@ -106,6 +106,9 @@ module API
project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
end
expose :autoclose_referenced_issues
expose :repository_storage, if: ->(project, options) {
Ability.allowed?(options[:current_user], :change_repository_storage, project)
}
# rubocop: disable CodeReuse/ActiveRecord
def self.preload_relation(projects_relation, options = {})
......
......@@ -54,6 +54,7 @@ module API
optional :auto_devops_enabled, type: Boolean, desc: 'Flag indication if Auto DevOps is enabled'
optional :auto_devops_deploy_strategy, type: String, values: %w(continuous manual timed_incremental), desc: 'Auto Deploy strategy'
optional :autoclose_referenced_issues, type: Boolean, desc: 'Flag indication if referenced issues auto-closing is enabled'
optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins'
end
params :optional_project_params_ee do
......@@ -125,6 +126,7 @@ module API
:wiki_access_level,
:avatar,
:suggestion_commit_message,
:repository_storage,
# TODO: remove in API v5, replaced by *_access_level
:issues_enabled,
......
......@@ -15,7 +15,7 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
segment ':id/commits/:commit_id' do
params do
requires :path, type: String, desc: 'The path of a file'
requires :paths, type: Array, desc: 'The paths of the files'
end
get 'lsif/info' do
authorize! :download_code, user_project
......@@ -30,7 +30,9 @@ module API
authorize! :read_pipeline, artifact.job.pipeline
file_too_large! if artifact.file.cached_size > MAX_FILE_SIZE
::Projects::LsifDataService.new(artifact.file, @project, params).execute
service = ::Projects::LsifDataService.new(artifact.file, @project, params[:commit_id])
params[:paths].to_h { |path| [path, service.execute(path)] }
end
end
end
......
......@@ -69,7 +69,16 @@ module API
end
params do
requires :repository_id, type: Integer, desc: 'The ID of the repository'
requires :name_regex, type: String, desc: 'The tag name regexp to delete, specify .* to delete all'
optional :name_regex_delete, type: String, desc: 'The tag name regexp to delete, specify .* to delete all'
# require either name_regex (deprecated) or name_regex_delete, it is ok to have both
given name_regex_delete: ->(val) { val.nil? } do
requires :name_regex, type: String, desc: 'The tag name regexp to delete, specify .* to delete all'
end
optional :name_regex, type: String, desc: 'The tag name regexp to delete, specify .* to delete all'
given name_regex: ->(val) { val.nil? } do
requires :name_regex_delete, type: String, desc: 'The tag name regexp to delete, specify .* to delete all'
end
optional :name_regex_keep, type: String, desc: 'The tag name regexp to retain'
optional :keep_n, type: Integer, desc: 'Keep n of latest tags with matching name'
optional :older_than, type: String, desc: 'Delete older than: 1h, 1d, 1month'
end
......
......@@ -25,6 +25,7 @@ module API
end
def verify_update_project_attrs!(project, attrs)
attrs.delete(:repository_storage) unless can?(current_user, :change_repository_storage, project)
end
def delete_project(user_project)
......
......@@ -118,10 +118,9 @@ module Gitlab
\.haml-lint_todo.yml |
babel\.config\.js |
jest\.config\.js |
karma\.config\.js |
webpack\.config\.js |
package\.json |
yarn\.lock |
config/.+\.js |
\.gitlab/ci/frontend\.gitlab-ci\.yml
)\z}x => :frontend,
......
namespace :gitlab do
namespace :cleanup do
desc "GitLab | Cleanup | Delete moved repositories"
task moved: :gitlab_environment do
warn_user_is_not_gitlab
remove_flag = ENV['REMOVE']
Gitlab.config.repositories.storages.each do |name, repository_storage|
repo_root = repository_storage.legacy_disk_path.chomp('/')
# Look for global repos (legacy, depth 1) and normal repos (depth 2)
IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *+moved*.git)) do |find|
find.each_line do |path|
path.chomp!
if remove_flag
if FileUtils.rm_rf(path)
puts "Removed...#{path}".color(:green)
else
puts "Cannot remove #{path}".color(:red)
end
else
puts "Can be removed: #{path}".color(:green)
end
end
end
end
unless remove_flag
puts "To cleanup these repositories run this command with REMOVE=true".color(:yellow)
end
end
end
end
......@@ -45,18 +45,20 @@ describe('Code navigation actions', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(apiUrl).replyOnce(200, [
{
start_line: 0,
start_char: 0,
hover: { value: '123' },
},
{
start_line: 1,
start_char: 0,
hover: null,
},
]);
mock.onGet(apiUrl).replyOnce(200, {
index: [
{
start_line: 0,
start_char: 0,
hover: { value: '123' },
},
{
start_line: 1,
start_char: 0,
hover: null,
},
],
});
});
it('commits REQUEST_DATA_SUCCESS with normalized data', done => {
......
......@@ -75,6 +75,16 @@ describe Resolvers::Projects::SnippetsResolver do
expect(resolve_snippets(context: { current_user: other_user }, args: { ids: project_snippet.to_global_id })).to be_empty
end
end
context 'when project snippets are disabled' do
it 'raises an error' do
disabled_snippet_project = create(:project, :snippets_disabled)
disabled_snippet_project.add_developer(current_user)
expect(SnippetsFinder).not_to receive(:new)
expect { resolve_snippets(obj: disabled_snippet_project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
def resolve_snippets(args: {}, context: { current_user: current_user }, obj: project)
......
......@@ -175,9 +175,12 @@ describe Gitlab::Danger::Helper do
'spec/javascripts/foo' | :frontend
'spec/frontend/bar' | :frontend
'vendor/assets/foo' | :frontend
'babel.config.js' | :frontend
'jest.config.js' | :frontend
'package.json' | :frontend
'yarn.lock' | :frontend
'config/foo.js' | :frontend
'config/deep/foo.js' | :frontend
'ee/app/assets/foo' | :frontend
'ee/app/views/foo' | :frontend
......
......@@ -2822,6 +2822,44 @@ describe Project do
end
end
describe '#change_repository_storage' do
let(:project) { create(:project, :repository) }
let(:read_only_project) { create(:project, :repository, repository_read_only: true) }
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
it 'schedules the transfer of the repository to the new storage and locks the project' do
expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'test_second_storage')
project.change_repository_storage('test_second_storage')
project.save!
expect(project).to be_repository_read_only
end
it "doesn't schedule the transfer if the repository is already read-only" do
expect(ProjectUpdateRepositoryStorageWorker).not_to receive(:perform_async)
read_only_project.change_repository_storage('test_second_storage')
read_only_project.save!
end
it "doesn't lock or schedule the transfer if the storage hasn't changed" do
expect(ProjectUpdateRepositoryStorageWorker).not_to receive(:perform_async)
project.change_repository_storage(project.repository_storage)
project.save!
expect(project).not_to be_repository_read_only
end
it 'throws an error if an invalid repository storage is provided' do
expect { project.change_repository_storage('unknown') }.to raise_error(ArgumentError)
end
end
describe '#pushes_since_gc' do
let(:project) { create(:project) }
......
......@@ -4,36 +4,73 @@ require 'spec_helper'
describe SnippetBlobPresenter do
describe '#rich_data' do
let(:snippet) { build(:personal_snippet) }
before do
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:current_user).and_return(nil)
end
end
subject { described_class.new(snippet.blob).rich_data }
it 'returns nil when the snippet blob is binary' do
allow(snippet.blob).to receive(:binary?).and_return(true)
context 'with PersonalSnippet' do
let(:raw_url) { "http://127.0.0.1:3000/snippets/#{snippet.id}/raw" }
let(:snippet) { build(:personal_snippet) }
expect(subject).to be_nil
end
it 'returns nil when the snippet blob is binary' do
allow(snippet.blob).to receive(:binary?).and_return(true)
it 'returns markdown content when snippet file is markup' do
snippet.file_name = 'test.md'
snippet.content = '*foo*'
expect(subject).to be_nil
end
expect(subject).to eq '<p data-sourcepos="1:1-1:5" dir="auto"><em>foo</em></p>'
end
context 'with markdown format' do
let(:snippet) { create(:personal_snippet, file_name: 'test.md', content: '*foo*') }
it 'returns syntax highlighted content' do
snippet.file_name = 'test.rb'
snippet.content = 'class Foo;end'
it 'returns rich markdown content' do
expected = <<~HTML
<div class="file-content md">
<p data-sourcepos="1:1-1:5" dir="auto"><em>foo</em></p>
</div>
HTML
expect(subject)
.to eq '<span id="LC1" class="line" lang="ruby"><span class="k">class</span> <span class="nc">Foo</span><span class="p">;</span><span class="k">end</span></span>'
end
expect(subject).to eq(expected)
end
end
it 'returns plain text highlighted content' do
snippet.file_name = 'test'
snippet.content = 'foo'
context 'with notebook format' do
let(:snippet) { create(:personal_snippet, file_name: 'test.ipynb') }
expect(subject).to eq '<span id="LC1" class="line" lang="plaintext">foo</span>'
it 'returns rich notebook content' do
expect(subject.strip).to eq %Q(<div class="file-content" data-endpoint="/snippets/#{snippet.id}/raw" id="js-notebook-viewer"></div>)
end
end
context 'with openapi format' do
let(:snippet) { create(:personal_snippet, file_name: 'openapi.yml') }
it 'returns rich openapi content' do
expect(subject).to eq %Q(<div class="file-content" data-endpoint="/snippets/#{snippet.id}/raw" id="js-openapi-viewer"></div>\n)
end
end
context 'with svg format' do
let(:snippet) { create(:personal_snippet, file_name: 'test.svg') }
it 'returns rich svg content' do
result = Nokogiri::HTML::DocumentFragment.parse(subject)
image_tag = result.search('img').first
expect(image_tag.attr('src')).to include("data:#{snippet.blob.mime_type};base64")
expect(image_tag.attr('alt')).to eq('test.svg')
end
end
context 'with other format' do
let(:snippet) { create(:personal_snippet, file_name: 'test') }
it 'does not return no rich content' do
expect(subject).to be_nil
end
end
end
end
......@@ -55,19 +92,19 @@ describe SnippetBlobPresenter do
expect(subject).to eq '<span id="LC1" class="line" lang="markdown"><span class="ge">*foo*</span></span>'
end
it 'returns plain syntax content' do
it 'returns highlighted syntax content' do
snippet.file_name = 'test.rb'
snippet.content = 'class Foo;end'
expect(subject)
.to eq '<span id="LC1" class="line" lang="">class Foo;end</span>'
.to eq '<span id="LC1" class="line" lang="ruby"><span class="k">class</span> <span class="nc">Foo</span><span class="p">;</span><span class="k">end</span></span>'
end
it 'returns plain text highlighted content' do
snippet.file_name = 'test'
snippet.content = 'foo'
expect(subject).to eq '<span id="LC1" class="line" lang="">foo</span>'
expect(subject).to eq '<span id="LC1" class="line" lang="plaintext">foo</span>'
end
end
......
......@@ -67,7 +67,7 @@ describe 'Creating a Snippet' do
it 'returns the created Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['blob']['richData']).to match(content)
expect(mutation_response['snippet']['blob']['richData']).to be_nil
expect(mutation_response['snippet']['blob']['plainData']).to match(content)
expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description)
......@@ -93,7 +93,7 @@ describe 'Creating a Snippet' do
it 'returns the created Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['blob']['richData']).to match(content)
expect(mutation_response['snippet']['blob']['richData']).to be_nil
expect(mutation_response['snippet']['blob']['plainData']).to match(content)
expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description)
......
......@@ -56,7 +56,7 @@ describe 'Updating a Snippet' do
it 'returns the updated Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['blob']['richData']).to match(updated_content)
expect(mutation_response['snippet']['blob']['richData']).to be_nil
expect(mutation_response['snippet']['blob']['plainData']).to match(updated_content)
expect(mutation_response['snippet']['title']).to eq(updated_title)
expect(mutation_response['snippet']['description']).to eq(updated_description)
......@@ -78,7 +78,7 @@ describe 'Updating a Snippet' do
it 'returns the Snippet with its original values' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['blob']['richData']).to match(original_content)
expect(mutation_response['snippet']['blob']['richData']).to be_nil
expect(mutation_response['snippet']['blob']['plainData']).to match(original_content)
expect(mutation_response['snippet']['title']).to eq(original_title)
expect(mutation_response['snippet']['description']).to eq(original_description)
......
......@@ -9,18 +9,20 @@ describe API::LsifData do
let(:commit) { project.commit }
describe 'GET lsif/info' do
let(:endpoint_path) { "/projects/#{project.id}/commits/#{commit.id}/lsif/info" }
subject do
endpoint_path = "/projects/#{project.id}/commits/#{commit.id}/lsif/info"
get api(endpoint_path, user), params: { paths: ['main.go', 'morestrings/reverse.go'] }
response
end
context 'user does not have access to the project' do
before do
project.add_guest(user)
end
it 'returns 403' do
get api(endpoint_path, user), params: { path: 'main.go' }
expect(response).to have_gitlab_http_status(:forbidden)
end
it { is_expected.to have_gitlab_http_status(:forbidden) }
end
context 'user has access to the project' do
......@@ -28,35 +30,27 @@ describe API::LsifData do
project.add_reporter(user)
end
context 'code_navigation feature is disabled' do
before do
stub_feature_flags(code_navigation: false)
end
it 'returns 404' do
get api(endpoint_path, user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'there is no job artifact for the passed commit' do
it 'returns 404' do
get api(endpoint_path, user), params: { path: 'main.go' }
expect(response).to have_gitlab_http_status(:not_found)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'lsif data is stored as a job artifact' do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: commit.id) }
let!(:artifact) { create(:ci_job_artifact, :lsif, job: create(:ci_build, pipeline: pipeline)) }
it 'returns code navigation info for a given path' do
get api(endpoint_path, user), params: { path: 'main.go' }
context 'code_navigation feature is disabled' do
before do
stub_feature_flags(code_navigation: false)
end
expect(response).to have_gitlab_http_status(:ok)
expect(response.parsed_body.last).to eq({
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it 'returns code navigation info for a given path', :aggregate_failures do
expect(subject).to have_gitlab_http_status(:ok)
data_for_main = response.parsed_body['main.go']
expect(data_for_main.last).to eq({
'end_char' => 18,
'end_line' => 8,
'start_char' => 13,
......@@ -67,26 +61,33 @@ describe API::LsifData do
'value' => Gitlab::Highlight.highlight(nil, 'func Func2(i int) string', language: 'go')
}]
})
data_for_reverse = response.parsed_body['morestrings/reverse.go']
expect(data_for_reverse.last).to eq({
'end_char' => 9,
'end_line' => 7,
'start_char' => 8,
'start_line' => 7,
'definition_url' => project_blob_path(project, "#{commit.id}/morestrings/reverse.go", anchor: 'L6'),
'hover' => [{
'language' => 'go',
'value' => Gitlab::Highlight.highlight(nil, 'var b string', language: 'go')
}]
})
end
context 'the stored file is too large' do
it 'returns 413' do
before do
allow_any_instance_of(JobArtifactUploader).to receive(:cached_size).and_return(20.megabytes)
get api(endpoint_path, user), params: { path: 'main.go' }
expect(response).to have_gitlab_http_status(:payload_too_large)
end
it { is_expected.to have_gitlab_http_status(:payload_too_large) }
end
context 'the user does not have access to the pipeline' do
let(:project) { create(:project, :repository, builds_access_level: ProjectFeature::DISABLED) }
it 'returns 403' do
get api(endpoint_path, user), params: { path: 'main.go' }
expect(response).to have_gitlab_http_status(:forbidden)
end
it { is_expected.to have_gitlab_http_status(:forbidden) }
end
end
end
......
......@@ -109,7 +109,7 @@ describe API::ProjectContainerRepositories do
context 'disallowed' do
let(:params) do
{ name_regex: 'v10.*' }
{ name_regex_delete: 'v10.*' }
end
it_behaves_like 'rejected container repository access', :developer, :forbidden
......@@ -130,16 +130,33 @@ describe API::ProjectContainerRepositories do
end
end
context 'without name_regex' do
let(:params) do
{ keep_n: 100,
older_than: '1 day',
other: 'some value' }
end
it 'returns bad request' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'passes all declared parameters' do
let(:params) do
{ name_regex: 'v10.*',
{ name_regex_delete: 'v10.*',
name_regex_keep: 'v10.1.*',
keep_n: 100,
older_than: '1 day',
other: 'some value' }
end
let(:worker_params) do
{ name_regex: 'v10.*',
{ name_regex: nil,
name_regex_delete: 'v10.*',
name_regex_keep: 'v10.1.*',
keep_n: 100,
older_than: '1 day',
container_expiration_policy: false }
......@@ -174,6 +191,38 @@ describe API::ProjectContainerRepositories do
end
end
end
context 'with deprecated name_regex param' do
let(:params) do
{ name_regex: 'v10.*',
name_regex_keep: 'v10.1.*',
keep_n: 100,
older_than: '1 day',
other: 'some value' }
end
let(:worker_params) do
{ name_regex: 'v10.*',
name_regex_delete: nil,
name_regex_keep: 'v10.1.*',
keep_n: 100,
older_than: '1 day',
container_expiration_policy: false }
end
let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
it 'schedules cleanup of tags repository' do
stub_last_activity_update
stub_exclusive_lease(lease_key, timeout: 1.hour)
expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
.with(maintainer.id, root_repository.id, worker_params)
subject
expect(response).to have_gitlab_http_status(:accepted)
end
end
end
end
......
......@@ -1751,6 +1751,27 @@ describe API::Projects do
subject { get api("/projects/#{project.id}", user) }
end
describe 'repository_storage attribute' do
before do
get api("/projects/#{project.id}", user)
end
context 'when authenticated as an admin' do
let(:user) { create(:admin) }
it 'returns repository_storage attribute' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['repository_storage']).to eq(project.repository_storage)
end
end
context 'when authenticated as a regular user' do
it 'does not return repository_storage attribute' do
expect(json_response).not_to have_key('repository_storage')
end
end
end
end
describe 'GET /projects/:id/users' do
......@@ -2402,6 +2423,50 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when updating repository storage' do
let(:unknown_storage) { 'new-storage' }
let(:new_project) { create(:project, :repository, namespace: user.namespace) }
context 'as a user' do
it 'returns 200 but does not change repository_storage' do
expect do
Sidekiq::Testing.fake! do
put(api("/projects/#{new_project.id}", user), params: { repository_storage: unknown_storage, issues_enabled: false })
end
end.not_to change(ProjectUpdateRepositoryStorageWorker.jobs, :size)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issues_enabled']).to eq(false)
expect(new_project.reload.repository.storage).to eq('default')
end
end
context 'as an admin' do
include_context 'custom session'
let(:admin) { create(:admin) }
it 'returns 500 when repository storage is unknown' do
put(api("/projects/#{new_project.id}", admin), params: { repository_storage: unknown_storage })
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(json_response['message']).to match('ArgumentError')
end
it 'returns 200 when repository storage has changed' do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/second_storage' })
expect do
Sidekiq::Testing.fake! do
put(api("/projects/#{new_project.id}", admin), params: { repository_storage: 'test_second_storage' })
end
end.to change(ProjectUpdateRepositoryStorageWorker.jobs, :size).by(1)
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
describe 'POST /projects/:id/archive' do
......
......@@ -48,25 +48,37 @@ describe Projects::ContainerRepository::CleanupTagsService do
end
context 'when regex matching everything is specified' do
shared_examples 'removes all matches' do
it 'does remove B* and C' do
# The :A cannot be removed as config is shared with :latest
# The :E cannot be removed as it does not have valid manifest
expect_delete('sha256:configB').twice
expect_delete('sha256:configC')
expect_delete('sha256:configD')
is_expected.to include(status: :success, deleted: %w(D Bb Ba C))
end
end
let(:params) do
{ 'name_regex' => '.*' }
{ 'name_regex_delete' => '.*' }
end
it 'does remove B* and C' do
# The :A cannot be removed as config is shared with :latest
# The :E cannot be removed as it does not have valid manifest
it_behaves_like 'removes all matches'
expect_delete('sha256:configB').twice
expect_delete('sha256:configC')
expect_delete('sha256:configD')
context 'with deprecated name_regex param' do
let(:params) do
{ 'name_regex' => '.*' }
end
is_expected.to include(status: :success, deleted: %w(D Bb Ba C))
it_behaves_like 'removes all matches'
end
end
context 'when regex matching specific tags is used' do
context 'when delete regex matching specific tags is used' do
let(:params) do
{ 'name_regex' => 'C|D' }
{ 'name_regex_delete' => 'C|D' }
end
it 'does remove C and D' do
......@@ -75,11 +87,37 @@ describe Projects::ContainerRepository::CleanupTagsService do
is_expected.to include(status: :success, deleted: %w(D C))
end
context 'with overriding allow regex' do
let(:params) do
{ 'name_regex_delete' => 'C|D',
'name_regex_keep' => 'C' }
end
it 'does not remove C' do
expect_delete('sha256:configD')
is_expected.to include(status: :success, deleted: %w(D))
end
end
context 'with name_regex_delete overriding deprecated name_regex' do
let(:params) do
{ 'name_regex' => 'C|D',
'name_regex_delete' => 'D' }
end
it 'does not remove C' do
expect_delete('sha256:configD')
is_expected.to include(status: :success, deleted: %w(D))
end
end
end
context 'when removing a tagged image that is used by another tag' do
let(:params) do
{ 'name_regex' => 'Ba' }
{ 'name_regex_delete' => 'Ba' }
end
it 'does not remove the tag' do
......@@ -89,9 +127,23 @@ describe Projects::ContainerRepository::CleanupTagsService do
end
end
context 'with allow regex value' do
let(:params) do
{ 'name_regex_delete' => '.*',
'name_regex_keep' => 'B.*' }
end
it 'does not remove B*' do
expect_delete('sha256:configC')
expect_delete('sha256:configD')
is_expected.to include(status: :success, deleted: %w(D C))
end
end
context 'when removing keeping only 3' do
let(:params) do
{ 'name_regex' => '.*',
{ 'name_regex_delete' => '.*',
'keep_n' => 3 }
end
......@@ -104,7 +156,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
context 'when removing older than 1 day' do
let(:params) do
{ 'name_regex' => '.*',
{ 'name_regex_delete' => '.*',
'older_than' => '1 day' }
end
......@@ -118,7 +170,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
context 'when combining all parameters' do
let(:params) do
{ 'name_regex' => '.*',
{ 'name_regex_delete' => '.*',
'keep_n' => 1,
'older_than' => '1 day' }
end
......@@ -136,7 +188,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
context 'with valid container_expiration_policy param' do
let(:params) do
{ 'name_regex' => '.*',
{ 'name_regex_delete' => '.*',
'keep_n' => 1,
'older_than' => '1 day',
'container_expiration_policy' => true }
......@@ -152,7 +204,7 @@ describe Projects::ContainerRepository::CleanupTagsService do
context 'without container_expiration_policy param' do
let(:params) do
{ 'name_regex' => '.*',
{ 'name_regex_delete' => '.*',
'keep_n' => 1,
'older_than' => '1 day' }
end
......
......@@ -307,6 +307,27 @@ describe Projects::ForkService do
end
end
context 'when a project is already forked' do
it 'creates a new poolresository after the project is moved to a new shard' do
project = create(:project, :public, :repository)
fork_before_move = fork_project(project)
# Stub everything required to move a project to a Gitaly shard that does not exist
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/second_storage' })
allow_any_instance_of(Gitlab::Git::Repository).to receive(:fetch_repository_as_mirror).and_return(true)
Projects::UpdateRepositoryStorageService.new(project).execute('test_second_storage')
fork_after_move = fork_project(project)
pool_repository_before_move = PoolRepository.joins(:shard)
.where(source_project: project, shards: { name: 'default' }).first
pool_repository_after_move = PoolRepository.joins(:shard)
.where(source_project: project, shards: { name: 'test_second_storage' }).first
expect(fork_before_move.pool_repository).to eq(pool_repository_before_move)
expect(fork_after_move.pool_repository).to eq(pool_repository_after_move)
end
end
context 'when forking with object pools' do
let(:fork_from_project) { create(:project, :public) }
let(:forker) { create(:user) }
......
......@@ -7,9 +7,8 @@ describe Projects::LsifDataService do
let(:project) { build_stubbed(:project) }
let(:path) { 'main.go' }
let(:commit_id) { Digest::SHA1.hexdigest(SecureRandom.hex) }
let(:params) { { path: path, commit_id: commit_id } }
let(:service) { described_class.new(artifact.file, project, params) }
let(:service) { described_class.new(artifact.file, project, commit_id) }
describe '#execute' do
def highlighted_value(value)
......@@ -18,7 +17,7 @@ describe Projects::LsifDataService do
context 'fetched lsif file', :use_clean_rails_memory_store_caching do
it 'is cached' do
service.execute
service.execute(path)
cached_data = Rails.cache.fetch("project:#{project.id}:lsif:#{commit_id}")
......@@ -30,7 +29,7 @@ describe Projects::LsifDataService do
let(:path_prefix) { "/#{project.full_path}/-/blob/#{commit_id}" }
it 'returns lsif ranges for the file' do
expect(service.execute).to eq([
expect(service.execute(path)).to eq([
{
end_char: 9,
end_line: 6,
......@@ -87,7 +86,7 @@ describe Projects::LsifDataService do
let(:path) { 'morestrings/reverse.go' }
it 'returns lsif ranges for the file' do
expect(service.execute.first).to eq({
expect(service.execute(path).first).to eq({
end_char: 2,
end_line: 11,
start_char: 1,
......@@ -102,7 +101,7 @@ describe Projects::LsifDataService do
let(:path) { 'unknown.go' }
it 'returns nil' do
expect(service.execute).to eq(nil)
expect(service.execute(path)).to eq(nil)
end
end
end
......@@ -120,9 +119,7 @@ describe Projects::LsifDataService do
end
it 'fetches the document with the shortest absolute path' do
service.instance_variable_set(:@docs, docs)
expect(service.__send__(:doc_id)).to eq(3)
expect(service.__send__(:find_doc_id, docs, path)).to eq(3)
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::UpdateRepositoryStorageService do
include Gitlab::ShellAdapter
subject { described_class.new(project) }
describe "#execute" do
let(:time) { Time.now }
before do
allow(Time).to receive(:now).and_return(time)
end
context 'without wiki and design repository' do
let(:project) { create(:project, :repository, repository_read_only: true, wiki_enabled: false) }
context 'when the move succeeds' do
it 'moves the repository to the new storage and unmarks the repository as read only' do
old_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
project.repository.path_to_repo
end
expect_any_instance_of(Gitlab::Git::Repository).to receive(:fetch_repository_as_mirror)
.with(project.repository.raw).and_return(true)
subject.execute('test_second_storage')
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('test_second_storage')
expect(gitlab_shell.repository_exists?('default', old_path)).to be(false)
expect(project.project_repository.shard_name).to eq('test_second_storage')
end
end
context 'when the project is already on the target storage' do
it 'bails out and does nothing' do
expect do
subject.execute(project.repository_storage)
end.to raise_error(described_class::RepositoryAlreadyMoved)
end
end
context 'when the move fails' do
it 'unmarks the repository as read-only without updating the repository storage' do
expect_any_instance_of(Gitlab::Git::Repository).to receive(:fetch_repository_as_mirror)
.with(project.repository.raw).and_return(false)
expect(GitlabShellWorker).not_to receive(:perform_async)
subject.execute('test_second_storage')
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('default')
end
end
end
context 'with wiki repository' do
include_examples 'moves repository to another storage', 'wiki' do
let(:project) { create(:project, :repository, repository_read_only: true, wiki_enabled: true) }
let(:repository) { project.wiki.repository }
before do
project.create_wiki
end
end
end
context 'when a object pool was joined' do
let(:project) { create(:project, :repository, wiki_enabled: false, repository_read_only: true) }
let(:pool) { create(:pool_repository, :ready, source_project: project) }
it 'leaves the pool' do
allow_any_instance_of(Gitlab::Git::Repository).to receive(:fetch_repository_as_mirror).and_return(true)
subject.execute('test_second_storage')
expect(project.reload_pool_repository).to be_nil
end
end
end
end
......@@ -613,6 +613,25 @@ describe Projects::UpdateService do
end
end
describe 'repository_storage' do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:opts) { { repository_storage: 'test_second_storage' } }
it 'calls the change repository storage method if the storage changed' do
expect(project).to receive(:change_repository_storage).with('test_second_storage')
update_project(project, admin, opts).inspect
end
it "doesn't call the change repository storage for non-admin users" do
expect(project).not_to receive(:change_repository_storage)
update_project(project, user, opts).inspect
end
end
def update_project(project, user, opts)
described_class.new(project, user, opts).execute
end
......
# frozen_string_literal: true
RSpec.shared_examples 'moves repository to another storage' do |repository_type|
let(:project_repository_double) { double(:repository) }
let(:repository_double) { double(:repository) }
before do
# Default stub for non-specified params
allow(Gitlab::Git::Repository).to receive(:new).and_call_original
allow(Gitlab::Git::Repository).to receive(:new)
.with('test_second_storage', project.repository.raw.relative_path, project.repository.gl_repository, project.repository.full_path)
.and_return(project_repository_double)
allow(Gitlab::Git::Repository).to receive(:new)
.with('test_second_storage', repository.raw.relative_path, repository.gl_repository, repository.full_path)
.and_return(repository_double)
end
context 'when the move succeeds', :clean_gitlab_redis_shared_state do
before do
allow(project_repository_double)
.to receive(:fetch_repository_as_mirror)
.with(project.repository.raw)
.and_return(true)
allow(repository_double)
.to receive(:fetch_repository_as_mirror)
.with(repository.raw)
.and_return(true)
end
it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read only" do
old_project_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
project.repository.path_to_repo
end
old_repository_path = repository.full_path
subject.execute('test_second_storage')
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('test_second_storage')
expect(gitlab_shell.repository_exists?('default', old_project_repository_path)).to be(false)
expect(gitlab_shell.repository_exists?('default', old_repository_path)).to be(false)
end
context ':repack_after_shard_migration feature flag disabled' do
before do
stub_feature_flags(repack_after_shard_migration: false)
end
it 'does not enqueue a GC run' do
expect { subject.execute('test_second_storage') }
.not_to change(GitGarbageCollectWorker.jobs, :count)
end
end
context ':repack_after_shard_migration feature flag enabled' do
before do
stub_feature_flags(repack_after_shard_migration: true)
end
it 'does not enqueue a GC run if housekeeping is disabled' do
stub_application_setting(housekeeping_enabled: false)
expect { subject.execute('test_second_storage') }
.not_to change(GitGarbageCollectWorker.jobs, :count)
end
it 'enqueues a GC run' do
expect { subject.execute('test_second_storage') }
.to change(GitGarbageCollectWorker.jobs, :count).by(1)
end
end
end
context 'when the project is already on the target storage' do
it 'bails out and does nothing' do
expect do
subject.execute(project.repository_storage)
end.to raise_error(described_class::RepositoryAlreadyMoved)
end
end
context "when the move of the #{repository_type} repository fails" do
it 'unmarks the repository as read-only without updating the repository storage' do
allow(project_repository_double).to receive(:fetch_repository_as_mirror)
.with(project.repository.raw).and_return(true)
allow(repository_double).to receive(:fetch_repository_as_mirror)
.with(repository.raw).and_return(false)
expect(GitlabShellWorker).not_to receive(:perform_async)
subject.execute('test_second_storage')
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('default')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ProjectUpdateRepositoryStorageWorker do
let(:project) { create(:project, :repository) }
subject { described_class.new }
describe "#perform" do
it "calls the update repository storage service" do
expect_any_instance_of(Projects::UpdateRepositoryStorageService)
.to receive(:execute).with('new_storage')
subject.perform(project.id, 'new_storage')
end
it 'catches and logs RepositoryAlreadyMoved' do
expect(Rails.logger).to receive(:info).with(/repository already moved/)
expect { subject.perform(project.id, project.repository_storage) }.not_to raise_error
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment