Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
51da0a2d39
commit
5bd4297fd7
|
@ -25,7 +25,6 @@ Layout/ClosingHeredocIndentation:
|
|||
- 'app/graphql/mutations/merge_requests/set_wip.rb'
|
||||
- 'ee/db/geo/migrate/20180322062741_migrate_ci_job_artifacts_to_separate_registry.rb'
|
||||
- 'ee/lib/gitlab/geo/health_check.rb'
|
||||
- 'lib/gitlab/background_migration/populate_untracked_uploads.rb'
|
||||
- 'spec/features/merge_request/user_sees_diff_spec.rb'
|
||||
- 'spec/lib/gitlab/asciidoc_spec.rb'
|
||||
- 'spec/lib/gitlab/checks/project_moved_spec.rb'
|
||||
|
@ -1536,7 +1535,6 @@ Rails/SaveBang:
|
|||
- 'spec/support/helpers/stub_object_storage.rb'
|
||||
- 'spec/support/migrations_helpers/cluster_helpers.rb'
|
||||
- 'spec/support/migrations_helpers/namespaces_helper.rb'
|
||||
- 'spec/support/migrations_helpers/track_untracked_uploads_helpers.rb'
|
||||
- 'spec/support/shared_contexts/email_shared_context.rb'
|
||||
- 'spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb'
|
||||
- 'spec/support/shared_contexts/mailers/notify_shared_context.rb'
|
||||
|
|
|
@ -2185,7 +2185,7 @@ entry.
|
|||
- Replaced ACE with Monaco editor for Snippets. !25465
|
||||
- Add support for user Job Title. !25483
|
||||
- Add name_regex_keep param to container registry bulk delete API endpoint. !25484
|
||||
- Add Project template for Gatsby. !25486
|
||||
- Add Project template for Gatsby. !24192
|
||||
- Add filepath to ReleaseLink. !25512
|
||||
- Added Drop older active deployments project setting. !25520
|
||||
- Add filepath to release links API. !25533
|
||||
|
|
|
@ -167,7 +167,7 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
if (!this.editor) {
|
||||
this.editor = Editor.create(this.editorOptions);
|
||||
this.editor = Editor.create(this.$store, this.editorOptions);
|
||||
}
|
||||
this.initEditor();
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { mapActions } from 'vuex';
|
|||
import Translate from '~/vue_shared/translate';
|
||||
import { identity } from 'lodash';
|
||||
import ide from './components/ide.vue';
|
||||
import store from './stores';
|
||||
import { createStore } from './stores';
|
||||
import { createRouter } from './ide_router';
|
||||
import { parseBoolean } from '../lib/utils/common_utils';
|
||||
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
|
||||
|
@ -32,6 +32,7 @@ export function initIde(el, options = {}) {
|
|||
if (!el) return null;
|
||||
|
||||
const { rootComponent = ide, extendStore = identity } = options;
|
||||
const store = createStore();
|
||||
const router = createRouter(store);
|
||||
|
||||
return new Vue({
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { debounce } from 'lodash';
|
||||
import { editor as monacoEditor, KeyCode, KeyMod, Range } from 'monaco-editor';
|
||||
import store from '../stores';
|
||||
import DecorationsController from './decorations/controller';
|
||||
import DirtyDiffController from './diff/controller';
|
||||
import Disposable from './common/disposable';
|
||||
|
@ -20,14 +19,14 @@ function setupThemes() {
|
|||
}
|
||||
|
||||
export default class Editor {
|
||||
static create(options = {}) {
|
||||
static create(...args) {
|
||||
if (!this.editorInstance) {
|
||||
this.editorInstance = new Editor(options);
|
||||
this.editorInstance = new Editor(...args);
|
||||
}
|
||||
return this.editorInstance;
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
constructor(store, options = {}) {
|
||||
this.currentModel = null;
|
||||
this.instance = null;
|
||||
this.dirtyDiffController = null;
|
||||
|
@ -42,6 +41,7 @@ export default class Editor {
|
|||
...defaultDiffEditorOptions,
|
||||
...options,
|
||||
};
|
||||
this.store = store;
|
||||
|
||||
setupThemes();
|
||||
registerLanguages(...languages);
|
||||
|
@ -215,6 +215,7 @@ export default class Editor {
|
|||
}
|
||||
|
||||
addCommands() {
|
||||
const { store } = this;
|
||||
const getKeyCode = key => {
|
||||
const monacoKeyMod = key.indexOf('KEY_') === 0;
|
||||
|
||||
|
|
|
@ -33,5 +33,3 @@ export const createStoreOptions = () => ({
|
|||
});
|
||||
|
||||
export const createStore = () => new Vuex.Store(createStoreOptions());
|
||||
|
||||
export default createStore();
|
||||
|
|
|
@ -254,16 +254,8 @@ export default {
|
|||
{{ __('Environment') }}
|
||||
</gl-dropdown-header>
|
||||
<gl-dropdown-divider />
|
||||
<gl-search-box-by-type
|
||||
ref="monitorEnvironmentsDropdownSearch"
|
||||
class="m-2"
|
||||
@input="debouncedEnvironmentsSearch"
|
||||
/>
|
||||
<gl-loading-icon
|
||||
v-if="environmentsLoading"
|
||||
ref="monitorEnvironmentsDropdownLoading"
|
||||
:inline="true"
|
||||
/>
|
||||
<gl-search-box-by-type class="m-2" @input="debouncedEnvironmentsSearch" />
|
||||
<gl-loading-icon v-if="environmentsLoading" :inline="true" />
|
||||
<div v-else class="flex-fill overflow-auto">
|
||||
<gl-dropdown-item
|
||||
v-for="environment in filteredEnvironments"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Show meaningful message when applying inapplicable suggestion
|
||||
merge_request: 37267
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove app_server_type from top level usage ping
|
||||
merge_request: 37279
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Cleanup migration to drop temporary table untracked_files_for_uploads if exists
|
||||
merge_request: 37352
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Geo: Add file store indexes'
|
||||
merge_request: 37265
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix the MR number in CHANGELOG
|
||||
merge_request: 37399
|
||||
author:
|
||||
type: other
|
|
@ -1,5 +1,2 @@
|
|||
# Ignore table used temporarily in background migration
|
||||
ActiveRecord::SchemaDumper.ignore_tables = ["untracked_files_for_uploads"]
|
||||
|
||||
# Ignore dynamically managed partitions in static application schema
|
||||
ActiveRecord::SchemaDumper.ignore_tables += ["#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.*"]
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnMergeRequestDiffsExternalDiffStore < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :merge_request_diffs, :external_diff_store
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :merge_request_diffs, :external_diff_store
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnTerraformStatesFileStore < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :terraform_states, :file_store
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :terraform_states, :file_store
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnVulnerabilityExportsFileStore < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :vulnerability_exports, :file_store
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :vulnerability_exports, :file_store
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropTemporaryTableUntrackedFilesForUploadsIfExists < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
execute 'DROP TABLE IF EXISTS untracked_files_for_uploads'
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op - this table should not exist
|
||||
end
|
||||
end
|
|
@ -19717,6 +19717,8 @@ CREATE INDEX index_merge_request_diff_commits_on_sha ON public.merge_request_dif
|
|||
|
||||
CREATE UNIQUE INDEX index_merge_request_diff_files_on_mr_diff_id_and_order ON public.merge_request_diff_files USING btree (merge_request_diff_id, relative_order);
|
||||
|
||||
CREATE INDEX index_merge_request_diffs_on_external_diff_store ON public.merge_request_diffs USING btree (external_diff_store);
|
||||
|
||||
CREATE INDEX index_merge_request_diffs_on_merge_request_id_and_id ON public.merge_request_diffs USING btree (merge_request_id, id);
|
||||
|
||||
CREATE INDEX index_merge_request_diffs_on_merge_request_id_and_id_partial ON public.merge_request_diffs USING btree (merge_request_id, id) WHERE ((NOT stored_externally) OR (stored_externally IS NULL));
|
||||
|
@ -20487,6 +20489,8 @@ CREATE INDEX index_term_agreements_on_term_id ON public.term_agreements USING bt
|
|||
|
||||
CREATE INDEX index_term_agreements_on_user_id ON public.term_agreements USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_terraform_states_on_file_store ON public.terraform_states USING btree (file_store);
|
||||
|
||||
CREATE INDEX index_terraform_states_on_locked_by_user_id ON public.terraform_states USING btree (locked_by_user_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_terraform_states_on_project_id_and_name ON public.terraform_states USING btree (project_id, name);
|
||||
|
@ -20645,6 +20649,8 @@ CREATE INDEX index_vulnerabilities_on_updated_by_id ON public.vulnerabilities US
|
|||
|
||||
CREATE INDEX index_vulnerability_exports_on_author_id ON public.vulnerability_exports USING btree (author_id);
|
||||
|
||||
CREATE INDEX index_vulnerability_exports_on_file_store ON public.vulnerability_exports USING btree (file_store);
|
||||
|
||||
CREATE INDEX index_vulnerability_exports_on_group_id_not_null ON public.vulnerability_exports USING btree (group_id) WHERE (group_id IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_vulnerability_exports_on_project_id_not_null ON public.vulnerability_exports USING btree (project_id) WHERE (project_id IS NOT NULL);
|
||||
|
@ -23960,6 +23966,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200703124823
|
||||
20200703125016
|
||||
20200703154822
|
||||
20200703165434
|
||||
20200704143633
|
||||
20200704161600
|
||||
20200706005325
|
||||
|
@ -23984,5 +23991,8 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200715202659
|
||||
20200716044023
|
||||
20200716120419
|
||||
20200718040100
|
||||
20200718040200
|
||||
20200718040300
|
||||
\.
|
||||
|
||||
|
|
|
@ -1275,7 +1275,7 @@ check the value of the `$CI_PIPELINE_SOURCE` variable.
|
|||
| `schedule` | For [scheduled pipelines](../pipelines/schedules.md). |
|
||||
| `api` | For pipelines triggered by the [pipelines API](../../api/pipelines.md#create-a-new-pipeline). |
|
||||
| `external` | When using CI services other than GitLab. |
|
||||
| `pipelines` | For multi-project pipelines created by [using the API with `CI_JOB_TOKEN`](../triggers/README.md#when-used-with-multi-project-pipelines). |
|
||||
| `pipeline` | For multi-project pipelines created by [using the API with `CI_JOB_TOKEN`](../triggers/README.md#when-used-with-multi-project-pipelines). |
|
||||
| `chat` | For pipelines created by using a [GitLab ChatOps](../chatops/README.md) command. |
|
||||
| `webide` | For pipelines created by using the [WebIDE](../../user/project/web_ide/index.md). |
|
||||
| `merge_request_event` | For pipelines created when a merge request is created or updated. Required to enable [merge request pipelines](../merge_request_pipelines/index.md), [merged results pipelines](../merge_request_pipelines/pipelines_for_merged_results/index.md), and [merge trains](../merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md). |
|
||||
|
|
|
@ -822,9 +822,6 @@ The following is example content of the Usage Ping payload.
|
|||
"adapter": "postgresql",
|
||||
"version": "9.6.15"
|
||||
},
|
||||
"app_server": {
|
||||
"type": "console"
|
||||
},
|
||||
"avg_cycle_analytics": {
|
||||
"issue": {
|
||||
"average": 999,
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# This class processes a batch of rows in `untracked_files_for_uploads` by
|
||||
# adding each file to the `uploads` table if it does not exist.
|
||||
class PopulateUntrackedUploads
|
||||
def perform(start_id, end_id)
|
||||
return unless migrate?
|
||||
|
||||
files = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.where(id: start_id..end_id)
|
||||
processed_files = insert_uploads_if_needed(files)
|
||||
processed_files.delete_all
|
||||
|
||||
drop_temp_table_if_finished
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def migrate?
|
||||
Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.table_exists? &&
|
||||
Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Upload.table_exists?
|
||||
end
|
||||
|
||||
def insert_uploads_if_needed(files)
|
||||
filtered_files, error_files = filter_error_files(files)
|
||||
filtered_files = filter_existing_uploads(filtered_files)
|
||||
filtered_files = filter_deleted_models(filtered_files)
|
||||
insert(filtered_files)
|
||||
|
||||
processed_files = files.where.not(id: error_files.map(&:id))
|
||||
processed_files
|
||||
end
|
||||
|
||||
def filter_error_files(files)
|
||||
files.partition do |file|
|
||||
file.to_h
|
||||
true
|
||||
rescue => e
|
||||
msg = <<~MSG
|
||||
Error parsing path "#{file.path}":
|
||||
#{e.message}
|
||||
#{e.backtrace.join("\n ")}
|
||||
MSG
|
||||
Rails.logger.error(msg) # rubocop:disable Gitlab/RailsLogger
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def filter_existing_uploads(files)
|
||||
paths = files.map(&:upload_path)
|
||||
existing_paths = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Upload.where(path: paths).pluck(:path).to_set
|
||||
|
||||
files.reject do |file|
|
||||
existing_paths.include?(file.upload_path)
|
||||
end
|
||||
end
|
||||
|
||||
# There are files on disk that are not in the uploads table because their
|
||||
# model was deleted, and we don't delete the files on disk.
|
||||
def filter_deleted_models(files)
|
||||
ids = deleted_model_ids(files)
|
||||
|
||||
files.reject do |file|
|
||||
ids[file.model_type].include?(file.model_id)
|
||||
end
|
||||
end
|
||||
|
||||
def deleted_model_ids(files)
|
||||
ids = {
|
||||
'Appearance' => [],
|
||||
'Namespace' => [],
|
||||
'Note' => [],
|
||||
'Project' => [],
|
||||
'User' => []
|
||||
}
|
||||
|
||||
# group model IDs by model type
|
||||
files.each do |file|
|
||||
ids[file.model_type] << file.model_id
|
||||
end
|
||||
|
||||
ids.each do |model_type, model_ids|
|
||||
model_class = "Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::#{model_type}".constantize
|
||||
found_ids = model_class.where(id: model_ids.uniq).pluck(:id)
|
||||
deleted_ids = ids[model_type] - found_ids
|
||||
ids[model_type] = deleted_ids
|
||||
end
|
||||
|
||||
ids
|
||||
end
|
||||
|
||||
def insert(files)
|
||||
rows = files.map do |file|
|
||||
file.to_h.merge(created_at: 'NOW()')
|
||||
end
|
||||
|
||||
Gitlab::Database.bulk_insert('uploads', # rubocop:disable Gitlab/BulkInsert
|
||||
rows,
|
||||
disable_quote: :created_at)
|
||||
end
|
||||
|
||||
def drop_temp_table_if_finished
|
||||
if Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.all.empty? && !Rails.env.test? # Dropping a table intermittently breaks test cleanup
|
||||
Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.connection.drop_table(:untracked_files_for_uploads,
|
||||
if_exists: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,190 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
module PopulateUntrackedUploadsDependencies
|
||||
# This class is responsible for producing the attributes necessary to
|
||||
# track an uploaded file in the `uploads` table.
|
||||
class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength
|
||||
self.table_name = 'untracked_files_for_uploads'
|
||||
|
||||
# Ends with /:random_hex/:filename
|
||||
FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}.freeze
|
||||
FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/.freeze
|
||||
|
||||
# These regex patterns are tested against a relative path, relative to
|
||||
# the upload directory.
|
||||
# For convenience, if there exists a capture group in the pattern, then
|
||||
# it indicates the model_id.
|
||||
PATH_PATTERNS = [
|
||||
{
|
||||
pattern: %r{\A-/system/appearance/logo/(\d+)/},
|
||||
uploader: 'AttachmentUploader',
|
||||
model_type: 'Appearance'
|
||||
},
|
||||
{
|
||||
pattern: %r{\A-/system/appearance/header_logo/(\d+)/},
|
||||
uploader: 'AttachmentUploader',
|
||||
model_type: 'Appearance'
|
||||
},
|
||||
{
|
||||
pattern: %r{\A-/system/note/attachment/(\d+)/},
|
||||
uploader: 'AttachmentUploader',
|
||||
model_type: 'Note'
|
||||
},
|
||||
{
|
||||
pattern: %r{\A-/system/user/avatar/(\d+)/},
|
||||
uploader: 'AvatarUploader',
|
||||
model_type: 'User'
|
||||
},
|
||||
{
|
||||
pattern: %r{\A-/system/group/avatar/(\d+)/},
|
||||
uploader: 'AvatarUploader',
|
||||
model_type: 'Namespace'
|
||||
},
|
||||
{
|
||||
pattern: %r{\A-/system/project/avatar/(\d+)/},
|
||||
uploader: 'AvatarUploader',
|
||||
model_type: 'Project'
|
||||
},
|
||||
{
|
||||
pattern: FILE_UPLOADER_PATH,
|
||||
uploader: 'FileUploader',
|
||||
model_type: 'Project'
|
||||
}
|
||||
].freeze
|
||||
|
||||
def to_h
|
||||
@upload_hash ||= {
|
||||
path: upload_path,
|
||||
uploader: uploader,
|
||||
model_type: model_type,
|
||||
model_id: model_id,
|
||||
size: file_size,
|
||||
checksum: checksum
|
||||
}
|
||||
end
|
||||
|
||||
def upload_path
|
||||
# UntrackedFile#path is absolute, but Upload#path depends on uploader
|
||||
@upload_path ||=
|
||||
if uploader == 'FileUploader'
|
||||
# Path relative to project directory in uploads
|
||||
matchd = path_relative_to_upload_dir.match(FILE_UPLOADER_PATH)
|
||||
matchd[0].sub(%r{\A/}, '') # remove leading slash
|
||||
else
|
||||
path
|
||||
end
|
||||
end
|
||||
|
||||
def uploader
|
||||
matching_pattern_map[:uploader]
|
||||
end
|
||||
|
||||
def model_type
|
||||
matching_pattern_map[:model_type]
|
||||
end
|
||||
|
||||
def model_id
|
||||
return @model_id if defined?(@model_id)
|
||||
|
||||
pattern = matching_pattern_map[:pattern]
|
||||
matchd = path_relative_to_upload_dir.match(pattern)
|
||||
|
||||
# If something is captured (matchd[1] is not nil), it is a model_id
|
||||
# Only the FileUploader pattern will not match an ID
|
||||
@model_id = matchd[1] ? matchd[1].to_i : file_uploader_model_id
|
||||
end
|
||||
|
||||
def file_size
|
||||
File.size(absolute_path)
|
||||
end
|
||||
|
||||
def checksum
|
||||
Digest::SHA256.file(absolute_path).hexdigest
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def matching_pattern_map
|
||||
@matching_pattern_map ||= PATH_PATTERNS.find do |path_pattern_map|
|
||||
path_relative_to_upload_dir.match(path_pattern_map[:pattern])
|
||||
end
|
||||
|
||||
unless @matching_pattern_map
|
||||
raise "Unknown upload path pattern \"#{path}\""
|
||||
end
|
||||
|
||||
@matching_pattern_map
|
||||
end
|
||||
|
||||
def file_uploader_model_id
|
||||
matchd = path_relative_to_upload_dir.match(FULL_PATH_CAPTURE)
|
||||
not_found_msg = <<~MSG
|
||||
Could not capture project full_path from a FileUploader path:
|
||||
"#{path_relative_to_upload_dir}"
|
||||
MSG
|
||||
raise not_found_msg unless matchd
|
||||
|
||||
full_path = matchd[1]
|
||||
project = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Project.find_by_full_path(full_path)
|
||||
return unless project
|
||||
|
||||
project.id
|
||||
end
|
||||
|
||||
# Not including a leading slash
|
||||
def path_relative_to_upload_dir
|
||||
upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR
|
||||
base = %r{\A#{Regexp.escape(upload_dir)}/}
|
||||
@path_relative_to_upload_dir ||= path.sub(base, '')
|
||||
end
|
||||
|
||||
def absolute_path
|
||||
File.join(Gitlab.config.uploads.storage_path, path)
|
||||
end
|
||||
end
|
||||
|
||||
# Avoid using application code
|
||||
class Upload < ActiveRecord::Base
|
||||
self.table_name = 'uploads'
|
||||
end
|
||||
|
||||
# Avoid using application code
|
||||
class Appearance < ActiveRecord::Base
|
||||
self.table_name = 'appearances'
|
||||
end
|
||||
|
||||
# Avoid using application code
|
||||
class Namespace < ActiveRecord::Base
|
||||
self.table_name = 'namespaces'
|
||||
end
|
||||
|
||||
# Avoid using application code
|
||||
class Note < ActiveRecord::Base
|
||||
self.table_name = 'notes'
|
||||
end
|
||||
|
||||
# Avoid using application code
|
||||
class User < ActiveRecord::Base
|
||||
self.table_name = 'users'
|
||||
end
|
||||
|
||||
# Since project Markdown upload paths don't contain the project ID, we have to find the
|
||||
# project by its full_path. Due to MySQL/PostgreSQL differences, and historical reasons,
|
||||
# the logic is somewhat complex, so I've mostly copied it in here.
|
||||
class Project < ActiveRecord::Base
|
||||
self.table_name = 'projects'
|
||||
|
||||
def self.find_by_full_path(path)
|
||||
order_sql = Arel.sql("(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)")
|
||||
where_full_path_in(path).reorder(order_sql).take
|
||||
end
|
||||
|
||||
def self.where_full_path_in(path)
|
||||
where = "(LOWER(routes.path) = LOWER(#{connection.quote(path)}))"
|
||||
joins("INNER JOIN routes ON routes.source_id = projects.id AND routes.source_type = 'Project'").where(where)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,173 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# This class finds all non-hashed uploaded file paths and saves them to a
|
||||
# `untracked_files_for_uploads` table.
|
||||
class PrepareUntrackedUploads # rubocop:disable Metrics/ClassLength
|
||||
# For bulk_queue_background_migration_jobs_by_range
|
||||
include Database::MigrationHelpers
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
FIND_BATCH_SIZE = 500
|
||||
RELATIVE_UPLOAD_DIR = "uploads"
|
||||
ABSOLUTE_UPLOAD_DIR = File.join(
|
||||
Gitlab.config.uploads.storage_path,
|
||||
RELATIVE_UPLOAD_DIR
|
||||
)
|
||||
FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads'
|
||||
START_WITH_ROOT_REGEX = %r{\A#{Gitlab.config.uploads.storage_path}/}.freeze
|
||||
EXCLUDED_HASHED_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/@hashed/*"
|
||||
EXCLUDED_TMP_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/tmp/*"
|
||||
|
||||
# This class is used to iterate over batches of
|
||||
# `untracked_files_for_uploads` rows.
|
||||
class UntrackedFile < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'untracked_files_for_uploads'
|
||||
end
|
||||
|
||||
def perform
|
||||
ensure_temporary_tracking_table_exists
|
||||
|
||||
# Since Postgres < 9.5 does not have ON CONFLICT DO NOTHING, and since
|
||||
# doing inserts-if-not-exists without ON CONFLICT DO NOTHING would be
|
||||
# slow, start with an empty table for Postgres < 9.5.
|
||||
# That way we can do bulk inserts at ~30x the speed of individual
|
||||
# inserts (~20 minutes worth of inserts at GitLab.com scale instead of
|
||||
# ~10 hours).
|
||||
# In all other cases, installations will get both bulk inserts and the
|
||||
# ability for these jobs to retry without having to clear and reinsert.
|
||||
clear_untracked_file_paths unless can_bulk_insert_and_ignore_duplicates?
|
||||
|
||||
store_untracked_file_paths
|
||||
|
||||
if UntrackedFile.all.empty?
|
||||
drop_temp_table
|
||||
else
|
||||
schedule_populate_untracked_uploads_jobs
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_temporary_tracking_table_exists
|
||||
table_name = :untracked_files_for_uploads
|
||||
|
||||
unless ActiveRecord::Base.connection.table_exists?(table_name)
|
||||
UntrackedFile.connection.create_table table_name do |t|
|
||||
t.string :path, limit: 600, null: false
|
||||
t.index :path, unique: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def clear_untracked_file_paths
|
||||
UntrackedFile.delete_all
|
||||
end
|
||||
|
||||
def store_untracked_file_paths
|
||||
return unless Dir.exist?(ABSOLUTE_UPLOAD_DIR)
|
||||
|
||||
each_file_batch(ABSOLUTE_UPLOAD_DIR, FIND_BATCH_SIZE) do |file_paths|
|
||||
insert_file_paths(file_paths)
|
||||
end
|
||||
end
|
||||
|
||||
def each_file_batch(search_dir, batch_size, &block)
|
||||
cmd = build_find_command(search_dir)
|
||||
|
||||
Open3.popen2(*cmd) do |stdin, stdout, status_thread|
|
||||
yield_paths_in_batches(stdout, batch_size, &block)
|
||||
|
||||
raise "Find command failed" unless status_thread.value.success?
|
||||
end
|
||||
end
|
||||
|
||||
def yield_paths_in_batches(stdout, batch_size, &block)
|
||||
paths = []
|
||||
|
||||
stdout.each_line("\0") do |line|
|
||||
paths << line.chomp("\0").sub(START_WITH_ROOT_REGEX, '')
|
||||
|
||||
if paths.size >= batch_size
|
||||
yield(paths)
|
||||
paths = []
|
||||
end
|
||||
end
|
||||
|
||||
yield(paths) if paths.any?
|
||||
end
|
||||
|
||||
def build_find_command(search_dir)
|
||||
cmd = %W[find -L #{search_dir}
|
||||
-type f
|
||||
! ( -path #{EXCLUDED_HASHED_UPLOADS_PATH} -prune )
|
||||
! ( -path #{EXCLUDED_TMP_UPLOADS_PATH} -prune )
|
||||
-print0]
|
||||
|
||||
ionice = which_ionice
|
||||
cmd = %W[#{ionice} -c Idle] + cmd if ionice
|
||||
|
||||
log_msg = "PrepareUntrackedUploads find command: \"#{cmd.join(' ')}\""
|
||||
Rails.logger.info log_msg # rubocop:disable Gitlab/RailsLogger
|
||||
|
||||
cmd
|
||||
end
|
||||
|
||||
def which_ionice
|
||||
Gitlab::Utils.which('ionice')
|
||||
rescue StandardError
|
||||
# In this case, returning false is relatively safe,
|
||||
# even though it isn't very nice
|
||||
false
|
||||
end
|
||||
|
||||
def insert_file_paths(file_paths)
|
||||
sql = insert_sql(file_paths)
|
||||
|
||||
ActiveRecord::Base.connection.execute(sql)
|
||||
end
|
||||
|
||||
def insert_sql(file_paths)
|
||||
if postgresql_pre_9_5?
|
||||
"INSERT INTO #{table_columns_and_values_for_insert(file_paths)};"
|
||||
else
|
||||
"INSERT INTO #{table_columns_and_values_for_insert(file_paths)}"\
|
||||
" ON CONFLICT DO NOTHING;"
|
||||
end
|
||||
end
|
||||
|
||||
def table_columns_and_values_for_insert(file_paths)
|
||||
values = file_paths.map do |file_path|
|
||||
ActiveRecord::Base.send(:sanitize_sql_array, ['(?)', file_path]) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end.join(', ')
|
||||
|
||||
"#{UntrackedFile.table_name} (path) VALUES #{values}"
|
||||
end
|
||||
|
||||
def can_bulk_insert_and_ignore_duplicates?
|
||||
!postgresql_pre_9_5?
|
||||
end
|
||||
|
||||
def postgresql_pre_9_5?
|
||||
strong_memoize(:postgresql_pre_9_5) do
|
||||
Gitlab::Database.version.to_f < 9.5
|
||||
end
|
||||
end
|
||||
|
||||
def schedule_populate_untracked_uploads_jobs
|
||||
bulk_queue_background_migration_jobs_by_range(
|
||||
UntrackedFile, FOLLOW_UP_MIGRATION)
|
||||
end
|
||||
|
||||
def drop_temp_table
|
||||
unless Rails.env.test? # Dropping a table intermittently breaks test cleanup
|
||||
UntrackedFile.connection.drop_table(:untracked_files_for_uploads,
|
||||
if_exists: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -83,7 +83,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
unless suggestion.appliable?(cached: false)
|
||||
return _('A suggestion is not applicable.')
|
||||
return suggestion.inapplicable_reason(cached: false)
|
||||
end
|
||||
|
||||
unless latest_source_head?(suggestion)
|
||||
|
|
|
@ -261,19 +261,10 @@ module Gitlab
|
|||
database: {
|
||||
adapter: alt_usage_data { Gitlab::Database.adapter_name },
|
||||
version: alt_usage_data { Gitlab::Database.version }
|
||||
},
|
||||
app_server: { type: app_server_type }
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def app_server_type
|
||||
Gitlab::Runtime.identify.to_s
|
||||
rescue Gitlab::Runtime::IdentificationError => e
|
||||
Gitlab::AppLogger.error(e.message)
|
||||
Gitlab::ErrorTracking.track_exception(e)
|
||||
'unknown_app_server_type'
|
||||
end
|
||||
|
||||
def object_store_config(component)
|
||||
config = alt_usage_data(fallback: nil) do
|
||||
Settings[component]['object_store']
|
||||
|
|
|
@ -420,6 +420,12 @@ msgstr ""
|
|||
msgid "%{labelStart}Class:%{labelEnd} %{class}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{labelStart}Crash Address:%{labelEnd} %{crash_address}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{labelStart}Crash State:%{labelEnd} %{stacktrace_snippet}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{labelStart}Evidence:%{labelEnd} %{evidence}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1139,9 +1145,6 @@ msgstr ""
|
|||
msgid "A subscription will trigger a new pipeline on the default branch of this project when a pipeline successfully completes for a new tag on the %{default_branch_docs} of the subscribed project."
|
||||
msgstr ""
|
||||
|
||||
msgid "A suggestion is not applicable."
|
||||
msgstr ""
|
||||
|
||||
msgid "A user with write access to the source branch selected this option"
|
||||
msgstr ""
|
||||
|
||||
|
@ -6848,9 +6851,6 @@ msgstr ""
|
|||
msgid "Coverage Fuzzing"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crash State"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27761,6 +27761,9 @@ msgstr ""
|
|||
msgid "ciReport|%{linkStartTag}Learn more about Container Scanning %{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{linkStartTag}Learn more about Coverage Fuzzing %{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{linkStartTag}Learn more about DAST %{linkEndTag}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27836,6 +27839,12 @@ msgstr ""
|
|||
msgid "ciReport|Coverage Fuzzing"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Coverage Fuzzing Title"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Coverage fuzzing"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Create a merge request to implement this solution, or download and apply the patch manually."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { leftSidebarViews } from '~/ide/constants';
|
||||
import ActivityBar from '~/ide/components/activity_bar.vue';
|
||||
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
|
||||
import { resetStore } from '../helpers';
|
||||
|
||||
describe('IDE activity bar', () => {
|
||||
const Component = Vue.extend(ActivityBar);
|
||||
let vm;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
Vue.set(store.state.projects, 'abcproject', {
|
||||
web_url: 'testing',
|
||||
});
|
||||
|
@ -20,8 +22,6 @@ describe('IDE activity bar', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
describe('updateActivityBarView', () => {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import emptyState from '~/ide/components/commit_sidebar/empty_state.vue';
|
||||
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
|
||||
import { resetStore } from '../../helpers';
|
||||
|
||||
describe('IDE commit panel empty state', () => {
|
||||
let vm;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
const Component = Vue.extend(emptyState);
|
||||
|
||||
Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes');
|
||||
|
@ -19,8 +21,6 @@ describe('IDE commit panel empty state', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('renders no changes text when last commit message is empty', () => {
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import Vue from 'vue';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import { projectData } from 'jest/ide/mock_data';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
|
||||
import { leftSidebarViews } from '~/ide/constants';
|
||||
import { resetStore } from '../../helpers';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
describe('IDE commit form', () => {
|
||||
const Component = Vue.extend(CommitForm);
|
||||
let vm;
|
||||
let store;
|
||||
|
||||
const beginCommitButton = () => vm.$el.querySelector('[data-testid="begin-commit-button"]');
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
store.state.changedFiles.push('test');
|
||||
store.state.currentProjectId = 'abcproject';
|
||||
store.state.currentBranchId = 'master';
|
||||
|
@ -24,8 +25,6 @@ describe('IDE commit form', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('enables begin commit button when there are changes', () => {
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import Vue from 'vue';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
|
||||
import { file } from '../../helpers';
|
||||
import { removeWhitespace } from '../../../helpers/text_helper';
|
||||
|
||||
describe('Multi-file editor commit sidebar list collapsed', () => {
|
||||
let vm;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
const Component = Vue.extend(listCollapsed);
|
||||
|
||||
vm = createComponentWithStore(Component, store, {
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import Vue from 'vue';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
|
||||
import { file, resetStore } from '../../helpers';
|
||||
import { file } from '../../helpers';
|
||||
|
||||
describe('Multi-file editor commit sidebar list', () => {
|
||||
let store;
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
const Component = Vue.extend(commitSidebarList);
|
||||
|
||||
vm = createComponentWithStore(Component, store, {
|
||||
|
@ -26,8 +29,6 @@ describe('Multi-file editor commit sidebar list', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
describe('with a list of files', () => {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import Vue from 'vue';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import { resetStore } from 'jest/ide/helpers';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
|
||||
|
||||
describe('IDE commit sidebar radio group', () => {
|
||||
let vm;
|
||||
let store;
|
||||
|
||||
beforeEach(done => {
|
||||
store = createStore();
|
||||
|
||||
const Component = Vue.extend(radioGroup);
|
||||
|
||||
store.state.commit.commitAction = '2';
|
||||
|
@ -25,8 +27,6 @@ describe('IDE commit sidebar radio group', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('uses label if present', () => {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import successMessage from '~/ide/components/commit_sidebar/success_message.vue';
|
||||
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
|
||||
import { resetStore } from '../../helpers';
|
||||
|
||||
describe('IDE commit panel successful commit state', () => {
|
||||
let vm;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
const Component = Vue.extend(successMessage);
|
||||
|
||||
vm = createComponentWithStore(Component, store, {
|
||||
|
@ -19,8 +21,6 @@ describe('IDE commit panel successful commit state', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('renders last commit message when it exists', done => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import Vue from 'vue';
|
|||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import FileRowExtra from '~/ide/components/file_row_extra.vue';
|
||||
import { file, resetStore } from '../helpers';
|
||||
import { file } from '../helpers';
|
||||
|
||||
describe('IDE extra file row component', () => {
|
||||
let Component;
|
||||
|
@ -32,7 +32,6 @@ describe('IDE extra file row component', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
resetStore(vm.$store);
|
||||
|
||||
stagedFilesCount = 0;
|
||||
unstagedFilesCount = 0;
|
||||
|
|
|
@ -2,7 +2,7 @@ import Vue from 'vue';
|
|||
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import Bar from '~/ide/components/file_templates/bar.vue';
|
||||
import { resetStore, file } from '../../helpers';
|
||||
import { file } from '../../helpers';
|
||||
|
||||
describe('IDE file templates bar component', () => {
|
||||
let Component;
|
||||
|
@ -26,7 +26,6 @@ describe('IDE file templates bar component', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
describe('template type dropdown', () => {
|
||||
|
|
|
@ -3,7 +3,7 @@ import IdeReview from '~/ide/components/ide_review.vue';
|
|||
import { createStore } from '~/ide/stores';
|
||||
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
|
||||
import { trimText } from '../../helpers/text_helper';
|
||||
import { resetStore, file } from '../helpers';
|
||||
import { file } from '../helpers';
|
||||
import { projectData } from '../mock_data';
|
||||
|
||||
describe('IDE review mode', () => {
|
||||
|
@ -26,8 +26,6 @@ describe('IDE review mode', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('renders list of files', () => {
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import Vue from 'vue';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import ideSidebar from '~/ide/components/ide_side_bar.vue';
|
||||
import { leftSidebarViews } from '~/ide/constants';
|
||||
import { resetStore } from '../helpers';
|
||||
import { projectData } from '../mock_data';
|
||||
|
||||
describe('IdeSidebar', () => {
|
||||
let vm;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
const Component = Vue.extend(ideSidebar);
|
||||
|
||||
store.state.currentProjectId = 'abcproject';
|
||||
|
@ -20,8 +22,6 @@ describe('IdeSidebar', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('renders a sidebar', () => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import Vue from 'vue';
|
|||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import ide from '~/ide/components/ide.vue';
|
||||
import { file, resetStore } from '../helpers';
|
||||
import { file } from '../helpers';
|
||||
import { projectData } from '../mock_data';
|
||||
import extendStore from '~/ide/stores/extend';
|
||||
|
||||
|
@ -41,8 +41,6 @@ describe('ide component, empty repo', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('renders "New file" button in empty repo', done => {
|
||||
|
@ -63,8 +61,6 @@ describe('ide component, non-empty repo', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('shows error message when set', done => {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Vue from 'vue';
|
||||
import IdeTreeList from '~/ide/components/ide_tree_list.vue';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
|
||||
import { resetStore, file } from '../helpers';
|
||||
import { file } from '../helpers';
|
||||
import { projectData } from '../mock_data';
|
||||
|
||||
describe('IDE tree list', () => {
|
||||
|
@ -10,6 +10,7 @@ describe('IDE tree list', () => {
|
|||
const normalBranchTree = [file('fileName')];
|
||||
const emptyBranchTree = [];
|
||||
let vm;
|
||||
let store;
|
||||
|
||||
const bootstrapWithTree = (tree = normalBranchTree) => {
|
||||
store.state.currentProjectId = 'abcproject';
|
||||
|
@ -25,10 +26,12 @@ describe('IDE tree list', () => {
|
|||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
describe('normal branch', () => {
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import Vue from 'vue';
|
||||
import IdeTree from '~/ide/components/ide_tree.vue';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
|
||||
import { resetStore, file } from '../helpers';
|
||||
import { file } from '../helpers';
|
||||
import { projectData } from '../mock_data';
|
||||
|
||||
describe('IdeRepoTree', () => {
|
||||
let store;
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
const IdeRepoTree = Vue.extend(IdeTree);
|
||||
|
||||
store.state.currentProjectId = 'abcproject';
|
||||
|
@ -24,8 +27,6 @@ describe('IdeRepoTree', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('renders list of files', () => {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import Vue from 'vue';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import newDropdown from '~/ide/components/new_dropdown/index.vue';
|
||||
import { resetStore } from '../../helpers';
|
||||
|
||||
describe('new dropdown component', () => {
|
||||
let store;
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
const component = Vue.extend(newDropdown);
|
||||
|
||||
vm = createComponentWithStore(component, store, {
|
||||
|
@ -30,8 +32,6 @@ describe('new dropdown component', () => {
|
|||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
it('renders new file, upload and new directory links', () => {
|
||||
|
|
|
@ -1,25 +1,5 @@
|
|||
import * as pathUtils from 'path';
|
||||
import { decorateData } from '~/ide/stores/utils';
|
||||
import state from '~/ide/stores/state';
|
||||
import commitState from '~/ide/stores/modules/commit/state';
|
||||
import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
|
||||
import pipelinesState from '~/ide/stores/modules/pipelines/state';
|
||||
import branchesState from '~/ide/stores/modules/branches/state';
|
||||
import fileTemplatesState from '~/ide/stores/modules/file_templates/state';
|
||||
import paneState from '~/ide/stores/modules/pane/state';
|
||||
|
||||
export const resetStore = store => {
|
||||
const newState = {
|
||||
...state(),
|
||||
commit: commitState(),
|
||||
mergeRequests: mergeRequestsState(),
|
||||
pipelines: pipelinesState(),
|
||||
branches: branchesState(),
|
||||
fileTemplates: fileTemplatesState(),
|
||||
rightPane: paneState(),
|
||||
};
|
||||
store.replaceState(newState);
|
||||
};
|
||||
|
||||
export const file = (name = 'name', id = name, type = '', parent = null) =>
|
||||
decorateData({
|
||||
|
|
|
@ -2,14 +2,17 @@ import Editor from '~/ide/lib/editor';
|
|||
import DecorationsController from '~/ide/lib/decorations/controller';
|
||||
import Model from '~/ide/lib/common/model';
|
||||
import { file } from '../../helpers';
|
||||
import { createStore } from '~/ide/stores';
|
||||
|
||||
describe('Multi-file editor library decorations controller', () => {
|
||||
let editorInstance;
|
||||
let controller;
|
||||
let model;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
editorInstance = Editor.create();
|
||||
store = createStore();
|
||||
editorInstance = Editor.create(store);
|
||||
editorInstance.createInstance(document.createElement('div'));
|
||||
|
||||
controller = new DecorationsController(editorInstance);
|
||||
|
|
|
@ -4,6 +4,7 @@ import ModelManager from '~/ide/lib/common/model_manager';
|
|||
import DecorationsController from '~/ide/lib/decorations/controller';
|
||||
import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
|
||||
import { computeDiff } from '~/ide/lib/diff/diff';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { file } from '../../helpers';
|
||||
|
||||
describe('Multi-file editor library dirty diff controller', () => {
|
||||
|
@ -12,9 +13,12 @@ describe('Multi-file editor library dirty diff controller', () => {
|
|||
let modelManager;
|
||||
let decorationsController;
|
||||
let model;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
editorInstance = Editor.create();
|
||||
store = createStore();
|
||||
|
||||
editorInstance = Editor.create(store);
|
||||
editorInstance.createInstance(document.createElement('div'));
|
||||
|
||||
modelManager = new ModelManager();
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
Selection,
|
||||
} from 'monaco-editor';
|
||||
import Editor from '~/ide/lib/editor';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { defaultEditorOptions } from '~/ide/lib/editor_options';
|
||||
import { file } from '../helpers';
|
||||
|
||||
|
@ -12,6 +13,7 @@ describe('Multi-file editor library', () => {
|
|||
let instance;
|
||||
let el;
|
||||
let holder;
|
||||
let store;
|
||||
|
||||
const setNodeOffsetWidth = val => {
|
||||
Object.defineProperty(instance.instance.getDomNode(), 'offsetWidth', {
|
||||
|
@ -22,13 +24,14 @@ describe('Multi-file editor library', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
el = document.createElement('div');
|
||||
holder = document.createElement('div');
|
||||
el.appendChild(holder);
|
||||
|
||||
document.body.appendChild(el);
|
||||
|
||||
instance = Editor.create();
|
||||
instance = Editor.create(store);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -44,7 +47,7 @@ describe('Multi-file editor library', () => {
|
|||
});
|
||||
|
||||
it('creates instance returns cached instance', () => {
|
||||
expect(Editor.create()).toEqual(instance);
|
||||
expect(Editor.create(store)).toEqual(instance);
|
||||
});
|
||||
|
||||
describe('createInstance', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import store from '~/ide/stores';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import createFlash from '~/flash';
|
||||
import {
|
||||
getMergeRequestData,
|
||||
|
@ -10,7 +10,6 @@ import {
|
|||
} from '~/ide/stores/actions/merge_request';
|
||||
import service from '~/ide/services';
|
||||
import { leftSidebarViews, PERMISSION_READ_MR } from '~/ide/constants';
|
||||
import { resetStore } from '../../helpers';
|
||||
|
||||
const TEST_PROJECT = 'abcproject';
|
||||
const TEST_PROJECT_ID = 17;
|
||||
|
@ -18,9 +17,12 @@ const TEST_PROJECT_ID = 17;
|
|||
jest.mock('~/flash');
|
||||
|
||||
describe('IDE store merge request actions', () => {
|
||||
let store;
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
store.state.projects[TEST_PROJECT] = {
|
||||
|
@ -34,7 +36,6 @@ describe('IDE store merge request actions', () => {
|
|||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
resetStore(store);
|
||||
});
|
||||
|
||||
describe('getMergeRequestsForBranch', () => {
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from '~/monitoring/stores';
|
||||
import * as types from '~/monitoring/stores/mutation_types';
|
||||
import { GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
|
||||
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
|
||||
import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
|
||||
import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
|
||||
import { setupAllDashboards } from '../store_utils';
|
||||
import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
|
||||
import {
|
||||
environmentData,
|
||||
dashboardGitResponse,
|
||||
selfMonitoringDashboardGitResponse,
|
||||
dashboardHeaderProps,
|
||||
} from '../mock_data';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
|
||||
const mockProjectPath = 'https://path/to/project';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
redirectTo: jest.fn(),
|
||||
queryToObject: jest.fn(),
|
||||
|
@ -21,6 +26,12 @@ describe('Dashboard header', () => {
|
|||
let store;
|
||||
let wrapper;
|
||||
|
||||
const findEnvsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
|
||||
const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlDropdownItem);
|
||||
const findEnvsDropdownSearch = () => findEnvsDropdown().find(GlSearchBoxByType);
|
||||
const findEnvsDropdownSearchMsg = () => wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' });
|
||||
const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().find(GlLoadingIcon);
|
||||
|
||||
const findActionsMenu = () => wrapper.find('[data-testid="actions-menu"]');
|
||||
const findCreateDashboardMenuItem = () =>
|
||||
findActionsMenu().find('[data-testid="action-create-dashboard"]');
|
||||
|
@ -29,6 +40,10 @@ describe('Dashboard header', () => {
|
|||
const findDuplicateDashboardModal = () => wrapper.find(DuplicateDashboardModal);
|
||||
const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]');
|
||||
|
||||
const setSearchTerm = searchTerm => {
|
||||
store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
|
||||
};
|
||||
|
||||
const createShallowWrapper = (props = {}, options = {}) => {
|
||||
wrapper = shallowMount(DashboardHeader, {
|
||||
propsData: { ...dashboardHeaderProps, ...props },
|
||||
|
@ -45,6 +60,113 @@ describe('Dashboard header', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('environments dropdown', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper();
|
||||
});
|
||||
|
||||
it('shows the environments dropdown', () => {
|
||||
expect(findEnvsDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a search input', () => {
|
||||
expect(findEnvsDropdownSearch().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when environments data is not loaded', () => {
|
||||
beforeEach(() => {
|
||||
setupStoreWithDashboard(store);
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('there are no environments listed', () => {
|
||||
expect(findEnvsDropdownItems().length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when environments data is loaded', () => {
|
||||
const currentDashboard = dashboardGitResponse[0].path;
|
||||
|
||||
beforeEach(() => {
|
||||
setupStoreWithData(store);
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
store.state.monitoringDashboard.currentDashboard = currentDashboard;
|
||||
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('renders dropdown items with the environment name', () => {
|
||||
const path = `${mockProjectPath}/-/metrics/${encodeURIComponent(currentDashboard)}`;
|
||||
|
||||
findEnvsDropdownItems().wrappers.forEach((itemWrapper, index) => {
|
||||
const { name, id } = environmentData[index];
|
||||
const idParam = encodeURIComponent(id);
|
||||
|
||||
expect(itemWrapper.text()).toBe(name);
|
||||
expect(itemWrapper.attributes('href')).toBe(`${path}?environment=${idParam}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Note: This test is not working, .active does not show the active environment
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/230615
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('renders the environments dropdown with a single active element', () => {
|
||||
const activeItem = findEnvsDropdownItems().wrappers.filter(itemWrapper =>
|
||||
itemWrapper.find('.active').exists(),
|
||||
);
|
||||
|
||||
expect(activeItem.length).toBe(1);
|
||||
});
|
||||
|
||||
it('filters rendered dropdown items', () => {
|
||||
const searchTerm = 'production';
|
||||
const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
|
||||
setSearchTerm(searchTerm);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findEnvsDropdownItems().length).toBe(resultEnvs.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not filter dropdown items if search term is empty string', () => {
|
||||
const searchTerm = '';
|
||||
setSearchTerm(searchTerm);
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findEnvsDropdownItems().length).toBe(environmentData.length);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error message if search term doesn't match", () => {
|
||||
const searchTerm = 'does-not-exist';
|
||||
setSearchTerm(searchTerm);
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findEnvsDropdownSearchMsg().isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows loading element when environments fetch is still loading', () => {
|
||||
store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
|
||||
|
||||
return wrapper.vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
expect(findEnvsDropdownLoadingIcon().exists()).toBe(true);
|
||||
})
|
||||
.then(() => {
|
||||
store.commit(
|
||||
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
|
||||
environmentData,
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
expect(findEnvsDropdownLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
|
||||
beforeEach(() => {
|
||||
store.state.monitoringDashboard.projectPath = 'root/sandbox';
|
||||
|
@ -81,7 +203,7 @@ describe('Dashboard header', () => {
|
|||
});
|
||||
|
||||
it('is rendered if projectPath is set in store', () => {
|
||||
store.state.monitoringDashboard.projectPath = 'https://path/to/project';
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findActionsMenu().exists()).toBe(true);
|
||||
|
@ -93,7 +215,7 @@ describe('Dashboard header', () => {
|
|||
});
|
||||
|
||||
it('contains a modal', () => {
|
||||
store.state.monitoringDashboard.projectPath = 'https://path/to/project';
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findActionsMenu().contains(CreateDashboardModal)).toBe(true);
|
||||
|
@ -111,7 +233,7 @@ describe('Dashboard header', () => {
|
|||
'when the selected dashboard can be duplicated',
|
||||
dashboardPath => {
|
||||
it('contains a "Create New" menu item and a "Duplicate Dashboard" menu item', () => {
|
||||
store.state.monitoringDashboard.projectPath = 'https://path/to/project';
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
setupAllDashboards(store, dashboardPath);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
|
@ -131,7 +253,7 @@ describe('Dashboard header', () => {
|
|||
'when the selected dashboard cannot be duplicated',
|
||||
dashboardPath => {
|
||||
it('contains a "Create New" menu item and no "Duplicate Dashboard" menu item', () => {
|
||||
store.state.monitoringDashboard.projectPath = 'https://path/to/project';
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
setupAllDashboards(store, dashboardPath);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
|
@ -144,10 +266,8 @@ describe('Dashboard header', () => {
|
|||
});
|
||||
|
||||
describe('actions menu modals', () => {
|
||||
const url = 'https://path/to/project';
|
||||
|
||||
beforeEach(() => {
|
||||
store.state.monitoringDashboard.projectPath = url;
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
setupAllDashboards(store);
|
||||
|
||||
createShallowWrapper();
|
||||
|
@ -166,7 +286,7 @@ describe('Dashboard header', () => {
|
|||
});
|
||||
|
||||
it('"Create new dashboard" modal contains correct buttons', () => {
|
||||
expect(findCreateDashboardModal().props('projectPath')).toBe(url);
|
||||
expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath);
|
||||
});
|
||||
|
||||
it('"Duplicate Dashboard" opens up a modal', () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import Tracking from '~/tracking';
|
||||
import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
|
||||
import { GlModal, GlDropdownItem, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
|
||||
import { GlModal, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
|
||||
import { objectToQuery } from '~/lib/utils/url_utility';
|
||||
import VueDraggable from 'vuedraggable';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
@ -29,7 +29,7 @@ import {
|
|||
setupStoreWithDataForPanelCount,
|
||||
setupStoreWithLinks,
|
||||
} from '../store_utils';
|
||||
import { environmentData, dashboardGitResponse, storeVariables } from '../mock_data';
|
||||
import { dashboardGitResponse, storeVariables } from '../mock_data';
|
||||
import {
|
||||
metricsDashboardViewModel,
|
||||
metricsDashboardPanelCount,
|
||||
|
@ -46,12 +46,6 @@ describe('Dashboard', () => {
|
|||
let mock;
|
||||
|
||||
const findDashboardHeader = () => wrapper.find(DashboardHeader);
|
||||
const findEnvironmentsDropdown = () =>
|
||||
findDashboardHeader().find({ ref: 'monitorEnvironmentsDropdown' });
|
||||
const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
|
||||
const setSearchTerm = searchTerm => {
|
||||
store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
|
||||
};
|
||||
|
||||
const createShallowWrapper = (props = {}, options = {}) => {
|
||||
wrapper = shallowMount(Dashboard, {
|
||||
|
@ -90,28 +84,6 @@ describe('Dashboard', () => {
|
|||
}
|
||||
});
|
||||
|
||||
describe('no metrics are available yet', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper();
|
||||
});
|
||||
|
||||
it('shows the environment selector', () => {
|
||||
expect(findEnvironmentsDropdown().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no data found', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper();
|
||||
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('shows the environment selector dropdown', () => {
|
||||
expect(findEnvironmentsDropdown().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('request information to the server', () => {
|
||||
it('calls to set time range and fetch data', () => {
|
||||
createShallowWrapper({ hasMetrics: true });
|
||||
|
@ -149,17 +121,14 @@ describe('Dashboard', () => {
|
|||
});
|
||||
|
||||
it('fetches the metrics data with proper time window', () => {
|
||||
jest.spyOn(store, 'dispatch');
|
||||
|
||||
createMountedWrapper({ hasMetrics: true });
|
||||
|
||||
store.commit(
|
||||
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
|
||||
environmentData,
|
||||
);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(store.dispatch).toHaveBeenCalled();
|
||||
expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
'monitoringDashboard/setTimeRange',
|
||||
expect.objectContaining({ duration: { seconds: 28800 } }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -500,21 +469,6 @@ describe('Dashboard', () => {
|
|||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('renders the environments dropdown with a number of environments', () => {
|
||||
expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length);
|
||||
|
||||
findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
|
||||
const anchorEl = itemWrapper.find('a');
|
||||
if (anchorEl.exists()) {
|
||||
const href = anchorEl.attributes('href');
|
||||
const currentDashboard = encodeURIComponent(dashboardGitResponse[0].path);
|
||||
const environmentId = encodeURIComponent(environmentData[index].id);
|
||||
const url = `${TEST_HOST}/-/metrics/${currentDashboard}?environment=${environmentId}`;
|
||||
expect(href).toBe(url);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('it does not show loading icons in any group', () => {
|
||||
setupStoreWithData(store);
|
||||
|
||||
|
@ -524,16 +478,6 @@ describe('Dashboard', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Note: This test is not working, .active does not show the active environment
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('renders the environments dropdown with a single active element', () => {
|
||||
const activeItem = findAllEnvironmentsDropdownItems().wrappers.filter(itemWrapper =>
|
||||
itemWrapper.find('.active').exists(),
|
||||
);
|
||||
|
||||
expect(activeItem.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('star dashboards', () => {
|
||||
|
@ -615,16 +559,6 @@ describe('Dashboard', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('hides the environments dropdown list when there is no environments', () => {
|
||||
createMountedWrapper({ hasMetrics: true });
|
||||
|
||||
setupStoreWithDashboard(store);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findAllEnvironmentsDropdownItems()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the datetimepicker dropdown', () => {
|
||||
createMountedWrapper({ hasMetrics: true });
|
||||
|
||||
|
@ -811,100 +745,6 @@ describe('Dashboard', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('searchable environments dropdown', () => {
|
||||
beforeEach(() => {
|
||||
createMountedWrapper({ hasMetrics: true }, { attachToDocument: true });
|
||||
|
||||
setupStoreWithData(store);
|
||||
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders a search input', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find(DashboardHeader)
|
||||
.find({ ref: 'monitorEnvironmentsDropdownSearch' })
|
||||
.exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('renders dropdown items', () => {
|
||||
findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
|
||||
const anchorEl = itemWrapper.find('a');
|
||||
if (anchorEl.exists()) {
|
||||
expect(anchorEl.text()).toBe(environmentData[index].name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('filters rendered dropdown items', () => {
|
||||
const searchTerm = 'production';
|
||||
const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
|
||||
setSearchTerm(searchTerm);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findAllEnvironmentsDropdownItems().length).toEqual(resultEnvs.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not filter dropdown items if search term is empty string', () => {
|
||||
const searchTerm = '';
|
||||
setSearchTerm(searchTerm);
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error message if search term doesn't match", () => {
|
||||
const searchTerm = 'does-not-exist';
|
||||
setSearchTerm(searchTerm);
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(
|
||||
wrapper
|
||||
.find(DashboardHeader)
|
||||
.find({ ref: 'monitorEnvironmentsDropdownMsg' })
|
||||
.isVisible(),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows loading element when environments fetch is still loading', () => {
|
||||
store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
|
||||
|
||||
return wrapper.vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
expect(
|
||||
wrapper
|
||||
.find(DashboardHeader)
|
||||
.find({ ref: 'monitorEnvironmentsDropdownLoading' })
|
||||
.exists(),
|
||||
).toBe(true);
|
||||
})
|
||||
.then(() => {
|
||||
store.commit(
|
||||
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
|
||||
environmentData,
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
expect(
|
||||
wrapper
|
||||
.find(DashboardHeader)
|
||||
.find({ ref: 'monitorEnvironmentsDropdownLoading' })
|
||||
.exists(),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('drag and drop function', () => {
|
||||
const findDraggables = () => wrapper.findAll(VueDraggable);
|
||||
const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled'));
|
||||
|
|
|
@ -1,263 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile do
|
||||
include MigrationsHelpers::TrackUntrackedUploadsHelpers
|
||||
|
||||
let!(:appearances) { table(:appearances) }
|
||||
let!(:namespaces) { table(:namespaces) }
|
||||
let!(:projects) { table(:projects) }
|
||||
let!(:routes) { table(:routes) }
|
||||
let!(:uploads) { table(:uploads) }
|
||||
|
||||
before(:all) do
|
||||
ensure_temporary_tracking_table_exists
|
||||
end
|
||||
|
||||
describe '#upload_path' do
|
||||
def assert_upload_path(file_path, expected_upload_path)
|
||||
untracked_file = create_untracked_file(file_path)
|
||||
|
||||
expect(untracked_file.upload_path).to eq(expected_upload_path)
|
||||
end
|
||||
|
||||
context 'for an appearance logo file path' do
|
||||
it 'returns the file path relative to the CarrierWave root' do
|
||||
assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for an appearance header_logo file path' do
|
||||
it 'returns the file path relative to the CarrierWave root' do
|
||||
assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a pre-Markdown Note attachment file path' do
|
||||
it 'returns the file path relative to the CarrierWave root' do
|
||||
assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a user avatar file path' do
|
||||
it 'returns the file path relative to the CarrierWave root' do
|
||||
assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a group avatar file path' do
|
||||
it 'returns the file path relative to the CarrierWave root' do
|
||||
assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project avatar file path' do
|
||||
it 'returns the file path relative to the CarrierWave root' do
|
||||
assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
|
||||
it 'returns the file path relative to the project directory in uploads' do
|
||||
project = create_project
|
||||
random_hex = SecureRandom.hex
|
||||
|
||||
assert_upload_path("/#{get_full_path(project)}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#uploader' do
|
||||
def assert_uploader(file_path, expected_uploader)
|
||||
untracked_file = create_untracked_file(file_path)
|
||||
|
||||
expect(untracked_file.uploader).to eq(expected_uploader)
|
||||
end
|
||||
|
||||
context 'for an appearance logo file path' do
|
||||
it 'returns AttachmentUploader as a string' do
|
||||
assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for an appearance header_logo file path' do
|
||||
it 'returns AttachmentUploader as a string' do
|
||||
assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a pre-Markdown Note attachment file path' do
|
||||
it 'returns AttachmentUploader as a string' do
|
||||
assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a user avatar file path' do
|
||||
it 'returns AvatarUploader as a string' do
|
||||
assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a group avatar file path' do
|
||||
it 'returns AvatarUploader as a string' do
|
||||
assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project avatar file path' do
|
||||
it 'returns AvatarUploader as a string' do
|
||||
assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
|
||||
it 'returns FileUploader as a string' do
|
||||
project = create_project
|
||||
|
||||
assert_uploader("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#model_type' do
|
||||
def assert_model_type(file_path, expected_model_type)
|
||||
untracked_file = create_untracked_file(file_path)
|
||||
|
||||
expect(untracked_file.model_type).to eq(expected_model_type)
|
||||
end
|
||||
|
||||
context 'for an appearance logo file path' do
|
||||
it 'returns Appearance as a string' do
|
||||
assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for an appearance header_logo file path' do
|
||||
it 'returns Appearance as a string' do
|
||||
assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a pre-Markdown Note attachment file path' do
|
||||
it 'returns Note as a string' do
|
||||
assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a user avatar file path' do
|
||||
it 'returns User as a string' do
|
||||
assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a group avatar file path' do
|
||||
it 'returns Namespace as a string' do
|
||||
assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project avatar file path' do
|
||||
it 'returns Project as a string' do
|
||||
assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
|
||||
it 'returns Project as a string' do
|
||||
project = create_project
|
||||
|
||||
assert_model_type("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'Project')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#model_id' do
|
||||
def assert_model_id(file_path, expected_model_id)
|
||||
untracked_file = create_untracked_file(file_path)
|
||||
|
||||
expect(untracked_file.model_id).to eq(expected_model_id)
|
||||
end
|
||||
|
||||
context 'for an appearance logo file path' do
|
||||
it 'returns the ID as a string' do
|
||||
assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for an appearance header_logo file path' do
|
||||
it 'returns the ID as a string' do
|
||||
assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a pre-Markdown Note attachment file path' do
|
||||
it 'returns the ID as a string' do
|
||||
assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a user avatar file path' do
|
||||
it 'returns the ID as a string' do
|
||||
assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a group avatar file path' do
|
||||
it 'returns the ID as a string' do
|
||||
assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project avatar file path' do
|
||||
it 'returns the ID as a string' do
|
||||
assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
|
||||
it 'returns the ID as a string' do
|
||||
project = create_project
|
||||
|
||||
assert_model_id("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", project.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#file_size' do
|
||||
context 'for an appearance logo file path' do
|
||||
let(:appearance) { create_or_update_appearance(logo: true) }
|
||||
let(:untracked_file) { described_class.create!(path: get_uploads(appearance, 'Appearance').first.path) }
|
||||
|
||||
it 'returns the file size' do
|
||||
expect(untracked_file.file_size).to eq(1062)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project avatar file path' do
|
||||
let(:project) { create_project(avatar: true) }
|
||||
let(:untracked_file) { described_class.create!(path: get_uploads(project, 'Project').first.path) }
|
||||
|
||||
it 'returns the file size' do
|
||||
expect(untracked_file.file_size).to eq(1062)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
|
||||
let(:project) { create_project }
|
||||
let(:untracked_file) { create_untracked_file("/#{get_full_path(project)}/#{get_uploads(project, 'Project').first.path}") }
|
||||
|
||||
before do
|
||||
add_markdown_attachment(project)
|
||||
end
|
||||
|
||||
it 'returns the file size' do
|
||||
expect(untracked_file.file_size).to eq(1062)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_untracked_file(path_relative_to_upload_dir)
|
||||
described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}")
|
||||
end
|
||||
end
|
|
@ -1,254 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::PopulateUntrackedUploads do
|
||||
include MigrationsHelpers::TrackUntrackedUploadsHelpers
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
let!(:appearances) { table(:appearances) }
|
||||
let!(:namespaces) { table(:namespaces) }
|
||||
let!(:notes) { table(:notes) }
|
||||
let!(:projects) { table(:projects) }
|
||||
let!(:routes) { table(:routes) }
|
||||
let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) }
|
||||
let!(:uploads) { table(:uploads) }
|
||||
let!(:users) { table(:users) }
|
||||
|
||||
before do
|
||||
ensure_temporary_tracking_table_exists
|
||||
uploads.delete_all
|
||||
end
|
||||
|
||||
context 'with untracked files and tracked files in untracked_files_for_uploads' do
|
||||
let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) }
|
||||
let!(:user1) { create_user(avatar: true) }
|
||||
let!(:user2) { create_user(avatar: true) }
|
||||
let!(:project1) { create_project(avatar: true) }
|
||||
let!(:project2) { create_project(avatar: true) }
|
||||
|
||||
before do
|
||||
add_markdown_attachment(project1)
|
||||
add_markdown_attachment(project2)
|
||||
|
||||
# File records created by PrepareUntrackedUploads
|
||||
untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').first.path)
|
||||
untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').last.path)
|
||||
untracked_files_for_uploads.create!(path: get_uploads(user1, 'User').first.path)
|
||||
untracked_files_for_uploads.create!(path: get_uploads(user2, 'User').first.path)
|
||||
untracked_files_for_uploads.create!(path: get_uploads(project1, 'Project').first.path)
|
||||
untracked_files_for_uploads.create!(path: get_uploads(project2, 'Project').first.path)
|
||||
untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project1).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project1, 'Project').last.path}")
|
||||
untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project2).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project2, 'Project').last.path}")
|
||||
|
||||
# Untrack 4 files
|
||||
get_uploads(user2, 'User').delete_all
|
||||
get_uploads(project2, 'Project').delete_all # 2 files: avatar and a Markdown upload
|
||||
get_uploads(appearance, 'Appearance').where("path like '%header_logo%'").delete_all
|
||||
end
|
||||
|
||||
it 'adds untracked files to the uploads table' do
|
||||
expect do
|
||||
subject.perform(1, untracked_files_for_uploads.reorder(:id).last.id)
|
||||
end.to change { uploads.count }.from(4).to(8)
|
||||
|
||||
expect(get_uploads(user2, 'User').count).to eq(1)
|
||||
expect(get_uploads(project2, 'Project').count).to eq(2)
|
||||
expect(get_uploads(appearance, 'Appearance').count).to eq(2)
|
||||
end
|
||||
|
||||
it 'deletes rows after processing them' do
|
||||
expect(subject).to receive(:drop_temp_table_if_finished) # Don't drop the table so we can look at it
|
||||
|
||||
expect do
|
||||
subject.perform(1, untracked_files_for_uploads.last.id)
|
||||
end.to change { untracked_files_for_uploads.count }.from(8).to(0)
|
||||
end
|
||||
|
||||
it 'does not create duplicate uploads of already tracked files' do
|
||||
subject.perform(1, untracked_files_for_uploads.last.id)
|
||||
|
||||
expect(get_uploads(user1, 'User').count).to eq(1)
|
||||
expect(get_uploads(project1, 'Project').count).to eq(2)
|
||||
expect(get_uploads(appearance, 'Appearance').count).to eq(2)
|
||||
end
|
||||
|
||||
it 'uses the start and end batch ids [only 1st half]' do
|
||||
ids = untracked_files_for_uploads.all.order(:id).pluck(:id)
|
||||
start_id = ids[0]
|
||||
end_id = ids[3]
|
||||
|
||||
expect do
|
||||
subject.perform(start_id, end_id)
|
||||
end.to change { uploads.count }.from(4).to(6)
|
||||
|
||||
expect(get_uploads(user1, 'User').count).to eq(1)
|
||||
expect(get_uploads(user2, 'User').count).to eq(1)
|
||||
expect(get_uploads(appearance, 'Appearance').count).to eq(2)
|
||||
expect(get_uploads(project1, 'Project').count).to eq(2)
|
||||
expect(get_uploads(project2, 'Project').count).to eq(0)
|
||||
|
||||
# Only 4 have been either confirmed or added to uploads
|
||||
expect(untracked_files_for_uploads.count).to eq(4)
|
||||
end
|
||||
|
||||
it 'uses the start and end batch ids [only 2nd half]' do
|
||||
ids = untracked_files_for_uploads.all.order(:id).pluck(:id)
|
||||
start_id = ids[4]
|
||||
end_id = ids[7]
|
||||
|
||||
expect do
|
||||
subject.perform(start_id, end_id)
|
||||
end.to change { uploads.count }.from(4).to(6)
|
||||
|
||||
expect(get_uploads(user1, 'User').count).to eq(1)
|
||||
expect(get_uploads(user2, 'User').count).to eq(0)
|
||||
expect(get_uploads(appearance, 'Appearance').count).to eq(1)
|
||||
expect(get_uploads(project1, 'Project').count).to eq(2)
|
||||
expect(get_uploads(project2, 'Project').count).to eq(2)
|
||||
|
||||
# Only 4 have been either confirmed or added to uploads
|
||||
expect(untracked_files_for_uploads.count).to eq(4)
|
||||
end
|
||||
|
||||
it 'does not drop the temporary tracking table after processing the batch, if there are still untracked rows' do
|
||||
subject.perform(1, untracked_files_for_uploads.last.id - 1)
|
||||
|
||||
expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_truthy
|
||||
end
|
||||
|
||||
it 'drops the temporary tracking table after processing the batch, if there are no untracked rows left' do
|
||||
expect(subject).to receive(:drop_temp_table_if_finished)
|
||||
|
||||
subject.perform(1, untracked_files_for_uploads.last.id)
|
||||
end
|
||||
|
||||
it 'does not block a whole batch because of one bad path' do
|
||||
untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a")
|
||||
expect(untracked_files_for_uploads.count).to eq(9)
|
||||
expect(uploads.count).to eq(4)
|
||||
|
||||
subject.perform(1, untracked_files_for_uploads.last.id)
|
||||
|
||||
expect(untracked_files_for_uploads.count).to eq(1)
|
||||
expect(uploads.count).to eq(8)
|
||||
end
|
||||
|
||||
it 'an unparseable path is shown in error output' do
|
||||
bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a"
|
||||
untracked_files_for_uploads.create!(path: bad_path)
|
||||
|
||||
expect(Rails.logger).to receive(:error).with(/Error parsing path "#{bad_path}":/)
|
||||
|
||||
subject.perform(1, untracked_files_for_uploads.last.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no untracked files' do
|
||||
it 'does not add to the uploads table (and does not raise error)' do
|
||||
expect do
|
||||
subject.perform(1, 1000)
|
||||
end.not_to change { uploads.count }.from(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'upload outcomes for each path pattern' do
|
||||
shared_examples_for 'non_markdown_file' do
|
||||
let!(:expected_upload_attrs) { model_uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') }
|
||||
let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) }
|
||||
|
||||
before do
|
||||
model_uploads.delete_all
|
||||
end
|
||||
|
||||
it 'creates an Upload record' do
|
||||
expect do
|
||||
subject.perform(1, untracked_files_for_uploads.last.id)
|
||||
end.to change { model_uploads.count }.from(0).to(1)
|
||||
|
||||
expect(model_uploads.first.attributes).to include(expected_upload_attrs)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for an appearance logo file path' do
|
||||
let(:model) { create_or_update_appearance(logo: true) }
|
||||
let(:model_uploads) { get_uploads(model, 'Appearance') }
|
||||
|
||||
it_behaves_like 'non_markdown_file'
|
||||
end
|
||||
|
||||
context 'for an appearance header_logo file path' do
|
||||
let(:model) { create_or_update_appearance(header_logo: true) }
|
||||
let(:model_uploads) { get_uploads(model, 'Appearance') }
|
||||
|
||||
it_behaves_like 'non_markdown_file'
|
||||
end
|
||||
|
||||
context 'for a pre-Markdown Note attachment file path' do
|
||||
let(:model) { create_note(attachment: true) }
|
||||
let!(:expected_upload_attrs) { get_uploads(model, 'Note').first.attributes.slice('path', 'uploader', 'size', 'checksum') }
|
||||
let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) }
|
||||
|
||||
before do
|
||||
get_uploads(model, 'Note').delete_all
|
||||
end
|
||||
|
||||
# Can't use the shared example because Note doesn't have an `uploads` association
|
||||
it 'creates an Upload record' do
|
||||
expect do
|
||||
subject.perform(1, untracked_files_for_uploads.last.id)
|
||||
end.to change { get_uploads(model, 'Note').count }.from(0).to(1)
|
||||
|
||||
expect(get_uploads(model, 'Note').first.attributes).to include(expected_upload_attrs)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a user avatar file path' do
|
||||
let(:model) { create_user(avatar: true) }
|
||||
let(:model_uploads) { get_uploads(model, 'User') }
|
||||
|
||||
it_behaves_like 'non_markdown_file'
|
||||
end
|
||||
|
||||
context 'for a group avatar file path' do
|
||||
let(:model) { create_group(avatar: true) }
|
||||
let(:model_uploads) { get_uploads(model, 'Namespace') }
|
||||
|
||||
it_behaves_like 'non_markdown_file'
|
||||
end
|
||||
|
||||
context 'for a project avatar file path' do
|
||||
let(:model) { create_project(avatar: true) }
|
||||
let(:model_uploads) { get_uploads(model, 'Project') }
|
||||
|
||||
it_behaves_like 'non_markdown_file'
|
||||
end
|
||||
|
||||
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
|
||||
let(:model) { create_project }
|
||||
|
||||
before do
|
||||
# Upload the file
|
||||
add_markdown_attachment(model)
|
||||
|
||||
# Create the untracked_files_for_uploads record
|
||||
untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(model)}/#{get_uploads(model, 'Project').first.path}")
|
||||
|
||||
# Save the expected upload attributes
|
||||
@expected_upload_attrs = get_uploads(model, 'Project').first.attributes.slice('path', 'uploader', 'size', 'checksum')
|
||||
|
||||
# Untrack the file
|
||||
get_uploads(model, 'Project').delete_all
|
||||
end
|
||||
|
||||
it 'creates an Upload record' do
|
||||
expect do
|
||||
subject.perform(1, untracked_files_for_uploads.last.id)
|
||||
end.to change { get_uploads(model, 'Project').count }.from(0).to(1)
|
||||
|
||||
expect(get_uploads(model, 'Project').first.attributes).to include(@expected_upload_attrs)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,159 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work.
|
||||
RSpec.describe Gitlab::BackgroundMigration::PrepareUntrackedUploads do
|
||||
include MigrationsHelpers::TrackUntrackedUploadsHelpers
|
||||
|
||||
let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) }
|
||||
let!(:appearances) { table(:appearances) }
|
||||
let!(:namespaces) { table(:namespaces) }
|
||||
let!(:projects) { table(:projects) }
|
||||
let!(:routes) { table(:routes) }
|
||||
let!(:uploads) { table(:uploads) }
|
||||
let!(:users) { table(:users) }
|
||||
|
||||
around do |example|
|
||||
# Especially important so the follow-up migration does not get run
|
||||
Sidekiq::Testing.fake! do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'prepares the untracked_files_for_uploads table' do
|
||||
context 'when files were uploaded before and after hashed storage was enabled' do
|
||||
let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) }
|
||||
let!(:user) { create_user(avatar: true) }
|
||||
let!(:project1) { create_project(avatar: true) }
|
||||
let(:project2) { create_project } # instantiate after enabling hashed_storage
|
||||
|
||||
before do
|
||||
# Markdown upload before enabling hashed_storage
|
||||
add_markdown_attachment(project1)
|
||||
|
||||
# Markdown upload after enabling hashed_storage
|
||||
add_markdown_attachment(project2, hashed_storage: true)
|
||||
end
|
||||
|
||||
it 'has a path field long enough for really long paths' do
|
||||
described_class.new.perform
|
||||
|
||||
component = 'a' * 255
|
||||
|
||||
long_path = [
|
||||
'uploads',
|
||||
component, # project.full_path
|
||||
component # filename
|
||||
].flatten.join('/')
|
||||
|
||||
record = untracked_files_for_uploads.create!(path: long_path)
|
||||
expect(record.reload.path.size).to eq(519)
|
||||
end
|
||||
|
||||
it 'adds unhashed files to the untracked_files_for_uploads table' do
|
||||
described_class.new.perform
|
||||
|
||||
expect(untracked_files_for_uploads.count).to eq(5)
|
||||
end
|
||||
|
||||
it 'adds files with paths relative to CarrierWave.root' do
|
||||
described_class.new.perform
|
||||
untracked_files_for_uploads.all.each do |file|
|
||||
expect(file.path.start_with?('uploads/')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not add hashed files to the untracked_files_for_uploads table' do
|
||||
described_class.new.perform
|
||||
|
||||
hashed_file_path = get_uploads(project2, 'Project').find_by(uploader: 'FileUploader').path
|
||||
expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey
|
||||
end
|
||||
|
||||
it 'correctly schedules the follow-up background migration jobs' do
|
||||
described_class.new.perform
|
||||
|
||||
ids = described_class::UntrackedFile.all.order(:id).pluck(:id)
|
||||
expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(ids.first, ids.last)
|
||||
expect(BackgroundMigrationWorker.jobs.size).to eq(1)
|
||||
end
|
||||
|
||||
# E.g. from a previous failed run of this background migration
|
||||
context 'when there is existing data in untracked_files_for_uploads' do
|
||||
before do
|
||||
described_class.new.perform
|
||||
end
|
||||
|
||||
it 'does not error or produce duplicates of existing data' do
|
||||
expect do
|
||||
described_class.new.perform
|
||||
end.not_to change { untracked_files_for_uploads.count }.from(5)
|
||||
end
|
||||
end
|
||||
|
||||
# E.g. The installation is in use at the time of migration, and someone has
|
||||
# just uploaded a file
|
||||
context 'when there are files in /uploads/tmp' do
|
||||
let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
|
||||
|
||||
before do
|
||||
FileUtils.mkdir(File.dirname(tmp_file))
|
||||
FileUtils.touch(tmp_file)
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm(tmp_file)
|
||||
end
|
||||
|
||||
it 'does not add files from /uploads/tmp' do
|
||||
described_class.new.perform
|
||||
|
||||
expect(untracked_files_for_uploads.count).to eq(5)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the last batch size exactly matches the max batch size' do
|
||||
it 'does not raise error' do
|
||||
stub_const("#{described_class}::FIND_BATCH_SIZE", 5)
|
||||
|
||||
expect do
|
||||
described_class.new.perform
|
||||
end.not_to raise_error
|
||||
|
||||
expect(untracked_files_for_uploads.count).to eq(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# If running on Postgres 9.2 (like on CI), this whole context is skipped
|
||||
# since we're unable to use ON CONFLICT DO NOTHING or IGNORE.
|
||||
context "test bulk insert with ON CONFLICT DO NOTHING or IGNORE", if: described_class.new.send(:can_bulk_insert_and_ignore_duplicates?) do
|
||||
it_behaves_like 'prepares the untracked_files_for_uploads table'
|
||||
end
|
||||
|
||||
# If running on Postgres 9.2 (like on CI), the stubbed method has no effect.
|
||||
#
|
||||
# If running on Postgres 9.5+ or MySQL, then this context effectively tests
|
||||
# the bulk insert functionality without ON CONFLICT DO NOTHING or IGNORE.
|
||||
context 'test bulk insert without ON CONFLICT DO NOTHING or IGNORE' do
|
||||
before do
|
||||
allow_any_instance_of(described_class).to receive(:postgresql_pre_9_5?).and_return(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'prepares the untracked_files_for_uploads table'
|
||||
end
|
||||
|
||||
# Very new or lightly-used installations that are running this migration
|
||||
# may not have an upload directory because they have no uploads.
|
||||
context 'when no files were ever uploaded' do
|
||||
it 'deletes the `untracked_files_for_uploads` table (and does not raise error)' do
|
||||
background_migration = described_class.new
|
||||
|
||||
expect(background_migration).to receive(:drop_temp_table)
|
||||
|
||||
background_migration.perform
|
||||
end
|
||||
end
|
||||
end
|
|
@ -574,10 +574,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
subject { described_class.components_usage_data }
|
||||
|
||||
it 'gathers basic components usage data' do
|
||||
stub_runtime(:puma)
|
||||
stub_application_setting(container_registry_vendor: 'gitlab', container_registry_version: 'x.y.z')
|
||||
|
||||
expect(subject[:app_server][:type]).to eq('puma')
|
||||
expect(subject[:gitlab_pages][:enabled]).to eq(Gitlab.config.pages.enabled)
|
||||
expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION)
|
||||
expect(subject[:git][:version]).to eq(Gitlab::Git.version)
|
||||
|
@ -591,32 +589,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
expect(subject[:container_registry][:vendor]).to eq('gitlab')
|
||||
expect(subject[:container_registry][:version]).to eq('x.y.z')
|
||||
end
|
||||
|
||||
def stub_runtime(runtime)
|
||||
allow(Gitlab::Runtime).to receive(:identify).and_return(runtime)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.app_server_type' do
|
||||
subject { described_class.app_server_type }
|
||||
|
||||
it 'successfully identifies runtime and returns the identifier' do
|
||||
expect(Gitlab::Runtime).to receive(:identify).and_return(:runtime_identifier)
|
||||
|
||||
is_expected.to eq('runtime_identifier')
|
||||
end
|
||||
|
||||
context 'when runtime is not identified' do
|
||||
let(:exception) { Gitlab::Runtime::IdentificationError.new('exception message from runtime identify') }
|
||||
|
||||
it 'logs the exception and returns unknown app server type' do
|
||||
expect(Gitlab::Runtime).to receive(:identify).and_raise(exception)
|
||||
|
||||
expect(Gitlab::AppLogger).to receive(:error).with(exception.message)
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception)
|
||||
expect(subject).to eq('unknown_app_server_type')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.object_store_config' do
|
||||
|
|
|
@ -74,7 +74,7 @@ RSpec.describe API::Suggestions do
|
|||
put api(url, user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response).to eq({ 'message' => 'A suggestion is not applicable.' })
|
||||
expect(json_response).to eq({ 'message' => "Can't apply as this line was changed in a more recent version." })
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -133,7 +133,7 @@ RSpec.describe API::Suggestions do
|
|||
params: { ids: [suggestion.id, unappliable_suggestion.id] }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response).to eq({ 'message' => 'A suggestion is not applicable.' })
|
||||
expect(json_response).to eq({ 'message' => "Can't apply as this line was changed in a more recent version." })
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -609,40 +609,16 @@ RSpec.describe Suggestions::ApplyService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'suggestion is eligible to be outdated' do
|
||||
context 'suggestion is not appliable' do
|
||||
let(:inapplicable_reason) { "Can't apply this suggestion." }
|
||||
|
||||
it 'returns error message' do
|
||||
expect(suggestion).to receive(:outdated?) { true }
|
||||
expect(suggestion).to receive(:appliable?).and_return(false)
|
||||
expect(suggestion).to receive(:inapplicable_reason).and_return(inapplicable_reason)
|
||||
|
||||
result = apply_service.new(user, suggestion).execute
|
||||
|
||||
expect(result).to eq(message: 'A suggestion is not applicable.',
|
||||
status: :error)
|
||||
end
|
||||
end
|
||||
|
||||
context 'note is outdated' do
|
||||
before do
|
||||
allow(diff_note).to receive(:active?) { false }
|
||||
end
|
||||
|
||||
it 'returns error message' do
|
||||
result = apply_service.new(user, suggestion).execute
|
||||
|
||||
expect(result).to eq(message: 'A suggestion is not applicable.',
|
||||
status: :error)
|
||||
end
|
||||
end
|
||||
|
||||
context 'suggestion was already applied' do
|
||||
before do
|
||||
suggestion.update!(applied: true, commit_id: 'sha')
|
||||
end
|
||||
|
||||
it 'returns error message' do
|
||||
result = apply_service.new(user, suggestion).execute
|
||||
|
||||
expect(result).to eq(message: 'A suggestion is not applicable.',
|
||||
status: :error)
|
||||
expect(result).to eq(message: inapplicable_reason, status: :error)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MigrationsHelpers
|
||||
module TrackUntrackedUploadsHelpers
|
||||
PUBLIC_DIR = File.join(Rails.root, 'tmp', 'tests', 'public')
|
||||
UPLOADS_DIR = File.join(PUBLIC_DIR, 'uploads')
|
||||
SYSTEM_DIR = File.join(UPLOADS_DIR, '-', 'system')
|
||||
UPLOAD_FILENAME = 'image.png'.freeze
|
||||
FIXTURE_FILE_PATH = File.join(Rails.root, 'spec', 'fixtures', 'dk.png')
|
||||
FIXTURE_CHECKSUM = 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'.freeze
|
||||
|
||||
def create_or_update_appearance(logo: false, header_logo: false)
|
||||
appearance = appearances.first_or_create(title: 'foo', description: 'bar', logo: (UPLOAD_FILENAME if logo), header_logo: (UPLOAD_FILENAME if header_logo))
|
||||
|
||||
add_upload(appearance, 'Appearance', 'logo', 'AttachmentUploader') if logo
|
||||
add_upload(appearance, 'Appearance', 'header_logo', 'AttachmentUploader') if header_logo
|
||||
|
||||
appearance
|
||||
end
|
||||
|
||||
def create_group(avatar: false)
|
||||
index = unique_index(:group)
|
||||
group = namespaces.create(name: "group#{index}", path: "group#{index}", avatar: (UPLOAD_FILENAME if avatar))
|
||||
|
||||
add_upload(group, 'Group', 'avatar', 'AvatarUploader') if avatar
|
||||
|
||||
group
|
||||
end
|
||||
|
||||
def create_note(attachment: false)
|
||||
note = notes.create(attachment: (UPLOAD_FILENAME if attachment))
|
||||
|
||||
add_upload(note, 'Note', 'attachment', 'AttachmentUploader') if attachment
|
||||
|
||||
note
|
||||
end
|
||||
|
||||
def create_project(avatar: false)
|
||||
group = create_group
|
||||
project = projects.create(namespace_id: group.id, path: "project#{unique_index(:project)}", avatar: (UPLOAD_FILENAME if avatar))
|
||||
routes.create(path: "#{group.path}/#{project.path}", source_id: project.id, source_type: 'Project') # so Project.find_by_full_path works
|
||||
|
||||
add_upload(project, 'Project', 'avatar', 'AvatarUploader') if avatar
|
||||
|
||||
project
|
||||
end
|
||||
|
||||
def create_user(avatar: false)
|
||||
user = users.create(email: "foo#{unique_index(:user)}@bar.com", avatar: (UPLOAD_FILENAME if avatar), projects_limit: 100)
|
||||
|
||||
add_upload(user, 'User', 'avatar', 'AvatarUploader') if avatar
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def unique_index(name = :unnamed)
|
||||
@unique_index ||= {}
|
||||
@unique_index[name] ||= 0
|
||||
@unique_index[name] += 1
|
||||
end
|
||||
|
||||
def add_upload(model, model_type, attachment_type, uploader)
|
||||
file_path = upload_file_path(model, model_type, attachment_type)
|
||||
path_relative_to_public = file_path.sub("#{PUBLIC_DIR}/", '')
|
||||
create_file(file_path)
|
||||
|
||||
uploads.create!(
|
||||
size: 1062,
|
||||
path: path_relative_to_public,
|
||||
model_id: model.id,
|
||||
model_type: model_type == 'Group' ? 'Namespace' : model_type,
|
||||
uploader: uploader,
|
||||
checksum: FIXTURE_CHECKSUM
|
||||
)
|
||||
end
|
||||
|
||||
def add_markdown_attachment(project, hashed_storage: false)
|
||||
project_dir = hashed_storage ? hashed_project_uploads_dir(project) : legacy_project_uploads_dir(project)
|
||||
attachment_dir = File.join(project_dir, SecureRandom.hex)
|
||||
attachment_file_path = File.join(attachment_dir, UPLOAD_FILENAME)
|
||||
project_attachment_path_relative_to_project = attachment_file_path.sub("#{project_dir}/", '')
|
||||
create_file(attachment_file_path)
|
||||
|
||||
uploads.create!(
|
||||
size: 1062,
|
||||
path: project_attachment_path_relative_to_project,
|
||||
model_id: project.id,
|
||||
model_type: 'Project',
|
||||
uploader: 'FileUploader',
|
||||
checksum: FIXTURE_CHECKSUM
|
||||
)
|
||||
end
|
||||
|
||||
def legacy_project_uploads_dir(project)
|
||||
namespace = namespaces.find_by(id: project.namespace_id)
|
||||
File.join(UPLOADS_DIR, namespace.path, project.path)
|
||||
end
|
||||
|
||||
def hashed_project_uploads_dir(project)
|
||||
File.join(UPLOADS_DIR, '@hashed', 'aa', 'aaaaaaaaaaaa')
|
||||
end
|
||||
|
||||
def upload_file_path(model, model_type, attachment_type)
|
||||
dir = File.join(upload_dir(model_type.downcase, attachment_type.to_s), model.id.to_s)
|
||||
File.join(dir, UPLOAD_FILENAME)
|
||||
end
|
||||
|
||||
def upload_dir(model_type, attachment_type)
|
||||
File.join(SYSTEM_DIR, model_type, attachment_type)
|
||||
end
|
||||
|
||||
def create_file(path)
|
||||
File.delete(path) if File.exist?(path)
|
||||
FileUtils.mkdir_p(File.dirname(path))
|
||||
FileUtils.cp(FIXTURE_FILE_PATH, path)
|
||||
end
|
||||
|
||||
def get_uploads(model, model_type)
|
||||
uploads.where(model_type: model_type, model_id: model.id)
|
||||
end
|
||||
|
||||
def get_full_path(project)
|
||||
routes.find_by(source_id: project.id, source_type: 'Project').path
|
||||
end
|
||||
|
||||
def ensure_temporary_tracking_table_exists
|
||||
Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue