diff --git a/.gitlab/ci/pages.gitlab-ci.yml b/.gitlab/ci/pages.gitlab-ci.yml index 993ed21e39d7209f7b14d0255e258e44832c8bf4..7c6f25cfe6b145b67c0963737b7ba9bba0d19cad 100644 --- a/.gitlab/ci/pages.gitlab-ci.yml +++ b/.gitlab/ci/pages.gitlab-ci.yml @@ -5,7 +5,7 @@ pages: - .default-cache - .pages:rules stage: pages - dependencies: ["coverage", "karma", "gitlab:assets:compile pull-cache"] + dependencies: ["rspec:coverage", "karma", "gitlab:assets:compile pull-cache"] script: - mv public/ .public/ - mkdir public/ diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 26f7febb59b87adf879f8065730d7c58b19f8b62..2f014824c911ac83ffc7f21022d0cff99da836e1 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -202,7 +202,7 @@ gitlab:setup: paths: - log/development.log -coverage: +rspec:coverage: extends: - .rails-job-base - .rails:rules:ee-and-foss diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md index 2f6792c6bea30320e2ced45c39d5dd2118adcddf..911e071caee742e3f4cb7c0af18c05eb31b9280f 100644 --- a/CHANGELOG-EE.md +++ b/CHANGELOG-EE.md @@ -1,5 +1,9 @@ Please view this file on the master branch, on stable branches it's out of date. +## 12.8.5 + +- No changes. + ## 12.8.4 - No changes. diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 27e1778e9b60e22a6f8ba05bca165b0f1a5c41cf..5c2e03e4b9c17bbdaaf1fa9d62a32dd3d8b048a4 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -302,6 +302,10 @@ class Snippet < ApplicationRecord field != :content || MarkupHelper.gitlab_markdown?(file_name) end + def hexdigest + Digest::SHA256.hexdigest("#{title}#{description}#{created_at}#{updated_at}") + end + class << self # Searches for snippets with a matching title or file name. # diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb index 10580c510987dac720d0c8e75adf75f84a709b33..89098971a7d9c3c3f3db0cdfdd2b6950880bdb0f 100644 --- a/app/models/snippet_repository.rb +++ b/app/models/snippet_repository.rb @@ -18,6 +18,12 @@ class SnippetRepository < ApplicationRecord end end + def create_file(user, path, content, **options) + options[:actions] = transform_file_entries([{ file_path: path, content: content }]) + + capture_git_error { repository.multi_action(user, **options) } + end + def multi_files_action(user, files = [], **options) return if files.nil? || files.empty? diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 5a3eb4c21561f6ae87ceff1ff81ca235a4e92280..74d5af41a0407f9079baac03bc1ae89bd62de4d8 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -42,7 +42,7 @@ module Projects end def exporters - [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver] + [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver, snippets_repo_saver] end def version_saver @@ -73,6 +73,10 @@ module Projects Gitlab::ImportExport::LfsSaver.new(project: project, shared: shared) end + def snippets_repo_saver + Gitlab::ImportExport::SnippetsRepoSaver.new(current_user: current_user, project: project, shared: shared) + end + def cleanup FileUtils.rm_rf(shared.archive_path) if shared&.archive_path end diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index b8b5eacfab1d97416b518acf698c950025aefc53..5afe43d663632af27c02c1b3f901e014e497efcc 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -143,7 +143,7 @@ - issue_tracker = @project.external_issue_tracker = link_to issue_tracker.issue_tracker_path, class: 'shortcuts-external_tracker' do .nav-icon-container - = sprite_icon('issue-external') + = sprite_icon('external-link') %span.nav-item-name = issue_tracker.title %ul.sidebar-sub-level-items.is-fly-out-only @@ -319,7 +319,7 @@ = nav_link do = link_to external_wiki_url, class: 'shortcuts-external_wiki' do .nav-icon-container - = sprite_icon('issue-external') + = sprite_icon('external-link') %span.nav-item-name = _('External Wiki') %ul.sidebar-sub-level-items.is-fly-out-only diff --git a/changelogs/unreleased/198323-migrate-snippet-mentions-to-db-table.yml b/changelogs/unreleased/198323-migrate-snippet-mentions-to-db-table.yml deleted file mode 100644 index 7501e713c6cab8e4d269c33a370fc16b7972f93b..0000000000000000000000000000000000000000 --- a/changelogs/unreleased/198323-migrate-snippet-mentions-to-db-table.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Migrate mentions for snippet and snippet notes to snippet_user_mentions DB - table -merge_request: 23783 -author: -type: changed diff --git a/changelogs/unreleased/208827-replace-issue-external-icon-with-external-link.yml b/changelogs/unreleased/208827-replace-issue-external-icon-with-external-link.yml new file mode 100644 index 0000000000000000000000000000000000000000..a929facdbbc80a0dfb79362e0071372b7798703f --- /dev/null +++ b/changelogs/unreleased/208827-replace-issue-external-icon-with-external-link.yml @@ -0,0 +1,5 @@ +--- +title: Replace issue-external icon with external-link +merge_request: 208827 +author: +type: other diff --git a/changelogs/unreleased/fj-39201-import-export-project-snippets.yml b/changelogs/unreleased/fj-39201-import-export-project-snippets.yml new file mode 100644 index 0000000000000000000000000000000000000000..790052f3f209c6a8cae050bfffb9c257c2cab76a --- /dev/null +++ b/changelogs/unreleased/fj-39201-import-export-project-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Import/Export snippet repositories +merge_request: 24150 +author: +type: added diff --git a/db/post_migrate/20200127111953_cleanup_empty_snippet_user_mentions.rb b/db/post_migrate/20200127111953_cleanup_empty_snippet_user_mentions.rb deleted file mode 100644 index aad688fef3fc530e0163601311d1b2cd56f663d2..0000000000000000000000000000000000000000 --- a/db/post_migrate/20200127111953_cleanup_empty_snippet_user_mentions.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -class CleanupEmptySnippetUserMentions < ActiveRecord::Migration[5.2] - DOWNTIME = false - BATCH_SIZE = 10_000 - - class SnippetUserMention < ActiveRecord::Base - include EachBatch - - self.table_name = 'snippet_user_mentions' - end - - def up - # cleanup snippet user mentions with no actual mentions, - # re https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24586#note_285982468 - SnippetUserMention - .where(mentioned_users_ids: nil) - .where(mentioned_groups_ids: nil) - .where(mentioned_projects_ids: nil) - .each_batch(of: BATCH_SIZE) do |batch| - batch.delete_all - end - end - - def down - # no-op - end -end diff --git a/db/post_migrate/20200127131953_migrate_snippet_mentions_to_db.rb b/db/post_migrate/20200127131953_migrate_snippet_mentions_to_db.rb deleted file mode 100644 index e25c2c2982ad2a8e6b21264b20de19a8f21869f0..0000000000000000000000000000000000000000 --- a/db/post_migrate/20200127131953_migrate_snippet_mentions_to_db.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -class MigrateSnippetMentionsToDb < ActiveRecord::Migration[5.2] - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - DELAY = 2.minutes.to_i - BATCH_SIZE = 10_000 - MIGRATION = 'UserMentions::CreateResourceUserMention' - - JOIN = "LEFT JOIN snippet_user_mentions on snippets.id = snippet_user_mentions.snippet_id" - QUERY_CONDITIONS = "(description like '%@%' OR title like '%@%') AND snippet_user_mentions.snippet_id IS NULL" - - disable_ddl_transaction! - - class Snippet < ActiveRecord::Base - include EachBatch - - self.table_name = 'snippets' - end - - def up - Snippet - .joins(JOIN) - .where(QUERY_CONDITIONS) - .each_batch(of: BATCH_SIZE) do |batch, index| - range = batch.pluck(Arel.sql('MIN(snippets.id)'), Arel.sql('MAX(snippets.id)')).first - migrate_in(index * DELAY, MIGRATION, ['Snippet', JOIN, QUERY_CONDITIONS, false, *range]) - end - end - - def down - # no-op - end -end diff --git a/db/post_migrate/20200127141953_add_temporary_snippet_notes_with_mentions_index.rb b/db/post_migrate/20200127141953_add_temporary_snippet_notes_with_mentions_index.rb deleted file mode 100644 index ec9b8b76f6f8a7162bf8fb83fdd857a651b4babc..0000000000000000000000000000000000000000 --- a/db/post_migrate/20200127141953_add_temporary_snippet_notes_with_mentions_index.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class AddTemporarySnippetNotesWithMentionsIndex < ActiveRecord::Migration[5.2] - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - INDEX_NAME = 'snippet_mentions_temp_index' - INDEX_CONDITION = "note LIKE '%@%'::text AND notes.noteable_type = 'Snippet'" - - disable_ddl_transaction! - - def up - # create temporary index for notes with mentions, may take well over 1h - add_concurrent_index(:notes, :id, where: INDEX_CONDITION, name: INDEX_NAME) - end - - def down - remove_concurrent_index(:notes, :id, where: INDEX_CONDITION, name: INDEX_NAME) - end -end diff --git a/db/post_migrate/20200127151953_migrate_snippet_notes_mentions_to_db.rb b/db/post_migrate/20200127151953_migrate_snippet_notes_mentions_to_db.rb deleted file mode 100644 index 3795a96b426463a872060f663ab8496d0d969340..0000000000000000000000000000000000000000 --- a/db/post_migrate/20200127151953_migrate_snippet_notes_mentions_to_db.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -class MigrateSnippetNotesMentionsToDb < ActiveRecord::Migration[5.2] - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - DELAY = 2.minutes.to_i - BATCH_SIZE = 10_000 - MIGRATION = 'UserMentions::CreateResourceUserMention' - - INDEX_CONDITION = "note LIKE '%@%'::text AND notes.noteable_type = 'Snippet'" - QUERY_CONDITIONS = "#{INDEX_CONDITION} AND snippet_user_mentions.snippet_id IS NULL" - JOIN = 'INNER JOIN snippets ON snippets.id = notes.noteable_id LEFT JOIN snippet_user_mentions ON notes.id = snippet_user_mentions.note_id' - - disable_ddl_transaction! - - class Note < ActiveRecord::Base - include EachBatch - - self.table_name = 'notes' - end - - def up - Note - .joins(JOIN) - .where(QUERY_CONDITIONS) - .each_batch(of: BATCH_SIZE) do |batch, index| - range = batch.pluck(Arel.sql('MIN(notes.id)'), Arel.sql('MAX(notes.id)')).first - migrate_in(index * DELAY, MIGRATION, ['Snippet', JOIN, QUERY_CONDITIONS, true, *range]) - end - end - - def down - # no-op - # temporary index is to be dropped in a different migration in an upcoming release: - # https://gitlab.com/gitlab-org/gitlab/issues/196842 - end -end diff --git a/db/schema.rb b/db/schema.rb index a9cc6d89c6da5e82734d25a05efd1fc3e57995e8..8cfa949548fa7085a9820c1aa59846caa6d0c500 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2838,7 +2838,6 @@ ActiveRecord::Schema.define(version: 2020_03_04_160823) do t.index ["discussion_id"], name: "index_notes_on_discussion_id" t.index ["id"], name: "design_mentions_temp_index", where: "((note ~~ '%@%'::text) AND ((noteable_type)::text = 'DesignManagement::Design'::text))" t.index ["id"], name: "epic_mentions_temp_index", where: "((note ~~ '%@%'::text) AND ((noteable_type)::text = 'Epic'::text))" - t.index ["id"], name: "snippet_mentions_temp_index", where: "((note ~~ '%@%'::text) AND ((noteable_type)::text = 'Snippet'::text))" t.index ["line_code"], name: "index_notes_on_line_code" t.index ["note"], name: "index_notes_on_note_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["note"], name: "tmp_idx_on_promoted_notes", where: "(((noteable_type)::text = 'Issue'::text) AND (system IS TRUE) AND (note ~~ 'promoted to epic%'::text))" diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index e81ce4c15cbb64f078e6290f995a121ecfcbbab8..0d9c6024601cd6627a3a2f24a9eba3081adcfce5 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -234,6 +234,29 @@ Recommended `NameID` value: `OneLogin ID`. Set parameters according to the [assertions table](#assertions). +### Additional setup options + +GitLab [isn't limited to the SAML providers listed above](#my-identity-provider-isnt-listed) but your Identity Provider may require additional configuration, such as the following: + +| Field | Value | Notes | +|-------|-------|-------| +| SAML Profile | Web browser SSO profile | GitLab uses SAML to sign users in via their browser. We don't make requests direct to the Identity Provider. | +| SAML Request Binding | HTTP Redirect | GitLab (the service provider) redirects users to your Identity Provider with a base64 encoded `SAMLRequest` HTTP parameter. | +| SAML Response Binding | HTTP POST | Your Identity Provider responds to users with an HTTP form including the `SAMLResponse`, which a user's browser submits back to GitLab. | +| Sign SAML Response | Yes | We require this to prevent tampering. | +| X509 Certificate in response | Yes | This is used to sign the response and checked against the provided fingerprint. | +| Fingerprint Algorithm | SHA-1 | We need a SHA-1 hash of the certificate used to sign the SAML Response. | +| Signature Algorithm | SHA-1/SHA-256/SHA-384/SHA-512 | Also known as the Digest Method, this can be specified in the SAML response. It determines how a response is signed. | +| Encrypt SAML Assertion | No | TLS is used between your Identity Provider, the user's browser, and GitLab. | +| Sign SAML Assertion | Optional | We don't require Assertions to be signed. We validate their integrity by requiring the whole response to be signed. | +| Check SAML Request Signature | No | GitLab does not sign SAML requests, but does check the signature on the SAML response. | +| Default RelayState | Optional | The URL users should end up on after signing in via a button on your Identity Provider. | +| NameID Format | `Persistent` | See [details above](#nameid-format). | +| Additional URLs | | You may need to use the `Identifier` or `Assertion consumer service URL` in other fields on some providers. | +| Single Sign Out URL | | Not supported | + +If the information information you need isn't listed above you may wish to check our [troubleshooting docs below](#i-need-additional-information-to-configure-my-identity-provider). + ## Linking SAML to your existing GitLab.com account To link SAML to your existing GitLab.com account: @@ -320,3 +343,20 @@ To change which identity you sign in with, you can [unlink the previous SAML ide Getting both of these errors at the same time suggests the NameID capitalization provided by the Identity Provider didn't exactly match the previous value for that user. This can be prevented by configuring the [NameID](#nameid) to return a consistent value. Fixing this for an individual user involves [unlinking SAML in the GitLab account](#unlinking-accounts), although this will cause group membership and Todos to be lost. + +### My identity provider isn't listed + +Not a problem, the SAML standard means that a wide range of identity providers will work with GitLab. Unfortunately we aren't familiar with all of them so can only offer support configuring the [listed providers](#providers). + +### I need additional information to configure my identity provider + +Many SAML terms can vary between providers. It is possible that the information you are looking for is listed under another name. + +For more information, start with your Identity Provider's documentation. Look for their options and examples to see how they configure SAML. This can provide hints on what you'll need to configure GitLab to work with these providers. + +It can also help to look at our [more detailed docs for self-managed GitLab](../../../integration/saml.md). +SAML configuration for GitLab.com is mostly the same as for self-managed instances. +However, self-managed GitLab instances use a configuration file that supports more options as described in the external [OmniAuth SAML documentation](https://github.com/omniauth/omniauth-saml/). +Internally that uses the [`ruby-saml` library](https://github.com/onelogin/ruby-saml), so we sometimes check there to verify low level details of less commonly used options. + +It can also help to compare the XML response from your provider with our [example XML used for internal testing](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/spec/fixtures/saml/response.xml). diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 56261d133955ccde3db19dc7304df8720fa39198..2949a2d4fc8c06ec2674d708d9ea9646e15073d9 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -110,6 +110,7 @@ the **Download codes** button for storage in a safe place. If you choose to download them, the file will be called `gitlab-recovery-codes.txt`. If you lose the recovery codes or just want to generate new ones, you can do so +from the [two-factor authentication account settings page](#regenerate-2fa-recovery-codes) or [using SSH](#generate-new-recovery-codes-using-ssh). ## Logging in with 2FA Enabled diff --git a/lib/api/api.rb b/lib/api/api.rb index ca6e6de33429d7522242f4e9d4ea98917fb9b2d0..02b3fe7e03e5af104db75af78f479496eca59f11 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -45,7 +45,7 @@ module API before do Gitlab::ApplicationContext.push( - user: -> { current_user }, + user: -> { @current_user }, project: -> { @project }, namespace: -> { @group }, caller_id: route.origin diff --git a/lib/gitlab/background_migration/user_mentions/models/snippet.rb b/lib/gitlab/background_migration/user_mentions/models/snippet.rb deleted file mode 100644 index cdbada764292d9ca52a7b7a8806b27ab59ec290e..0000000000000000000000000000000000000000 --- a/lib/gitlab/background_migration/user_mentions/models/snippet.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true -# rubocop:disable Style/Documentation - -module Gitlab - module BackgroundMigration - module UserMentions - module Models - class Snippet < ActiveRecord::Base - include Concerns::IsolatedMentionable - include Concerns::MentionableMigrationMethods - include CacheMarkdownField - - attr_mentionable :title, pipeline: :single_line - attr_mentionable :description - cache_markdown_field :title, pipeline: :single_line - cache_markdown_field :description - - self.table_name = 'snippets' - self.inheritance_column = :_type_disabled - - belongs_to :author, class_name: "User" - belongs_to :project - - def self.user_mention_model - Gitlab::BackgroundMigration::UserMentions::Models::SnippetUserMention - end - - def user_mention_model - self.class.user_mention_model - end - - def user_mention_resource_id - id - end - - def user_mention_note_id - 'NULL' - end - end - end - end - end -end diff --git a/lib/gitlab/background_migration/user_mentions/models/snippet_user_mention.rb b/lib/gitlab/background_migration/user_mentions/models/snippet_user_mention.rb deleted file mode 100644 index a856a53626e30e728d1c7f566889da142f925f7d..0000000000000000000000000000000000000000 --- a/lib/gitlab/background_migration/user_mentions/models/snippet_user_mention.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true -# rubocop:disable Style/Documentation - -module Gitlab - module BackgroundMigration - module UserMentions - module Models - class SnippetUserMention < ActiveRecord::Base - self.table_name = 'snippet_user_mentions' - - def self.resource_foreign_key - :snippet_id - end - end - end - end - end -end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 1033e6c4e0588276729779242527945fe7fbab6b..52102b6f508f1aed910b38f89c6dc80108a67311 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -42,6 +42,18 @@ module Gitlab "project.wiki.bundle" end + def snippet_repo_bundle_dir + 'snippets' + end + + def snippets_repo_bundle_path(absolute_path) + File.join(absolute_path, ::Gitlab::ImportExport.snippet_repo_bundle_dir) + end + + def snippet_repo_bundle_filename_for(snippet) + "#{snippet.hexdigest}.bundle" + end + def config_file Rails.root.join('lib/gitlab/import_export/project/import_export.yml') end diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 4eeecc140673d06849e8133a877c53951b900c41..4b761eb86ae92f77760c7d71c73d69ab0f62e2f0 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -35,7 +35,7 @@ module Gitlab def restorers [repo_restorer, wiki_restorer, project_tree, avatar_restorer, - uploads_restorer, lfs_restorer, statistics_restorer] + uploads_restorer, lfs_restorer, statistics_restorer, snippets_repo_restorer] end def import_file @@ -79,6 +79,12 @@ module Gitlab Gitlab::ImportExport::LfsRestorer.new(project: project, shared: shared) end + def snippets_repo_restorer + Gitlab::ImportExport::SnippetsRepoRestorer.new(project: project, + shared: shared, + user: current_user) + end + def statistics_restorer Gitlab::ImportExport::StatisticsRestorer.new(project: project, shared: shared) end diff --git a/lib/gitlab/import_export/snippet_repo_restorer.rb b/lib/gitlab/import_export/snippet_repo_restorer.rb new file mode 100644 index 0000000000000000000000000000000000000000..079681dfac52a8a1cb14c0706182f569e96b71f7 --- /dev/null +++ b/lib/gitlab/import_export/snippet_repo_restorer.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + class SnippetRepoRestorer < RepoRestorer + attr_reader :snippet + + def initialize(snippet:, user:, shared:, path_to_bundle:) + @snippet = snippet + @user = user + @repository = snippet.repository + @path_to_bundle = path_to_bundle.to_s + @shared = shared + end + + def restore + if File.exist?(path_to_bundle) + create_repository_from_bundle + else + create_repository_from_db + end + + true + rescue => e + shared.error(e) + false + end + + private + + def create_repository_from_bundle + repository.create_from_bundle(path_to_bundle) + snippet.track_snippet_repository + end + + def create_repository_from_db + snippet.create_repository + + commit_attrs = { + branch_name: 'master', + message: 'Initial commit' + } + + repository.create_file(@user, snippet.file_name, snippet.content, commit_attrs) + end + end + end +end diff --git a/lib/gitlab/import_export/snippet_repo_saver.rb b/lib/gitlab/import_export/snippet_repo_saver.rb new file mode 100644 index 0000000000000000000000000000000000000000..cab96c782322a4460f438d2db9121a08b3c808e3 --- /dev/null +++ b/lib/gitlab/import_export/snippet_repo_saver.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + class SnippetRepoSaver < RepoSaver + def initialize(project:, shared:, repository:) + @project = project + @shared = shared + @repository = repository + end + + private + + def bundle_full_path + File.join(shared.export_path, + ::Gitlab::ImportExport.snippet_repo_bundle_dir, + ::Gitlab::ImportExport.snippet_repo_bundle_filename_for(repository.container)) + end + end + end +end diff --git a/lib/gitlab/import_export/snippets_repo_restorer.rb b/lib/gitlab/import_export/snippets_repo_restorer.rb new file mode 100644 index 0000000000000000000000000000000000000000..8fe83225812278d7a5b3a4451f0f23273e440646 --- /dev/null +++ b/lib/gitlab/import_export/snippets_repo_restorer.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + class SnippetsRepoRestorer + def initialize(project:, shared:, user:) + @project = project + @shared = shared + @user = user + end + + def restore + return true unless Feature.enabled?(:version_snippets, @user) + return true unless Dir.exist?(snippets_repo_bundle_path) + + @project.snippets.find_each.all? do |snippet| + Gitlab::ImportExport::SnippetRepoRestorer.new(snippet: snippet, + user: @user, + shared: @shared, + path_to_bundle: snippet_repo_bundle_path(snippet)) + .restore + end + end + + private + + def snippet_repo_bundle_path(snippet) + File.join(snippets_repo_bundle_path, ::Gitlab::ImportExport.snippet_repo_bundle_filename_for(snippet)) + end + + def snippets_repo_bundle_path + @snippets_repo_bundle_path ||= ::Gitlab::ImportExport.snippets_repo_bundle_path(@shared.export_path) + end + end + end +end diff --git a/lib/gitlab/import_export/snippets_repo_saver.rb b/lib/gitlab/import_export/snippets_repo_saver.rb new file mode 100644 index 0000000000000000000000000000000000000000..85e094c0d1567a78eb6b10039d519da73c17822d --- /dev/null +++ b/lib/gitlab/import_export/snippets_repo_saver.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + class SnippetsRepoSaver + include Gitlab::ImportExport::CommandLineUtil + + def initialize(current_user:, project:, shared:) + @project = project + @shared = shared + @current_user = current_user + end + + def save + return true unless Feature.enabled?(:version_snippets, @current_user) + + create_snippets_repo_directory + + @project.snippets.find_each.all? do |snippet| + Gitlab::ImportExport::SnippetRepoSaver.new(project: @project, + shared: @shared, + repository: snippet.repository) + .save + end + end + + private + + def create_snippets_repo_directory + mkdir_p(::Gitlab::ImportExport.snippets_repo_bundle_path(@shared.export_path)) + end + end + end +end diff --git a/scripts/review_apps/automated_cleanup.rb b/scripts/review_apps/automated_cleanup.rb index 323a1deedfc01f2d97bc3093ba92a4d0afde21fb..93412b658f602eec6cabcf5b047e1adc6ff254c3 100755 --- a/scripts/review_apps/automated_cleanup.rb +++ b/scripts/review_apps/automated_cleanup.rb @@ -81,7 +81,7 @@ class AutomatedCleanup release = Quality::HelmClient::Release.new(environment.slug, 1, deployed_at.to_s, nil, nil, review_apps_namespace) releases_to_delete << release end - elsif deployed_at < stop_threshold + elsif environment.state != 'stopped' && deployed_at < stop_threshold stop_environment(environment, deployment) else print_release_state(subject: 'Review App', release_name: environment.slug, release_date: last_deploy, action: 'leaving') diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb index 2cdb8019696953457a140391bc246f2d8dbf0264..8ab5c7f1fa58e8b73a965de7a5ada19bc977d8ce 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -34,7 +34,7 @@ FactoryBot.define do trait :empty_repo do after(:create) do |snippet| - raise "Failed to create repository!" unless snippet.repository.create_if_not_exists + raise "Failed to create repository!" unless snippet.create_repository end end end diff --git a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb deleted file mode 100644 index 9c085b3cef845bd1c943b89f89459dd4d255d4dc..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require './db/post_migrate/20200127131953_migrate_snippet_mentions_to_db' -require './db/post_migrate/20200127151953_migrate_snippet_notes_mentions_to_db' - -describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMention, schema: 20200127151953 do - include MigrationsHelpers - - context 'when migrating data' do - let(:users) { table(:users) } - let(:namespaces) { table(:namespaces) } - let(:projects) { table(:projects) } - let(:notes) { table(:notes) } - - let(:author) { users.create!(email: 'author@example.com', notification_email: 'author@example.com', name: 'author', username: 'author', projects_limit: 10, state: 'active') } - let(:member) { users.create!(email: 'member@example.com', notification_email: 'member@example.com', name: 'member', username: 'member', projects_limit: 10, state: 'active') } - let(:admin) { users.create!(email: 'administrator@example.com', notification_email: 'administrator@example.com', name: 'administrator', username: 'administrator', admin: 1, projects_limit: 10, state: 'active') } - let(:john_doe) { users.create!(email: 'john_doe@example.com', notification_email: 'john_doe@example.com', name: 'john_doe', username: 'john_doe', projects_limit: 10, state: 'active') } - let(:skipped) { users.create!(email: 'skipped@example.com', notification_email: 'skipped@example.com', name: 'skipped', username: 'skipped', projects_limit: 10, state: 'active') } - - let(:mentioned_users) { [author, member, admin, john_doe, skipped] } - let(:mentioned_users_refs) { mentioned_users.map { |u| "@#{u.username}" }.join(' ') } - - let(:group) { namespaces.create!(name: 'test1', path: 'test1', runners_token: 'my-token1', project_creation_level: 1, visibility_level: 20, type: 'Group') } - let(:inaccessible_group) { namespaces.create!(name: 'test2', path: 'test2', runners_token: 'my-token2', project_creation_level: 1, visibility_level: 0, type: 'Group') } - let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) } - - let(:mentioned_groups) { [group, inaccessible_group] } - let(:group_mentions) { [group, inaccessible_group].map { |gr| "@#{gr.path}" }.join(' ') } - let(:description_mentions) { "description with mentions #{mentioned_users_refs} and #{group_mentions}" } - - before do - # build personal namespaces and routes for users - mentioned_users.each { |u| u.becomes(User).save! } - - # build namespaces and routes for groups - mentioned_groups.each do |gr| - gr.name += '-org' - gr.path += '-org' - gr.becomes(Namespace).save! - end - end - - context 'migrate snippet mentions' do - let(:snippets) { table(:snippets) } - let(:snippet_user_mentions) { table(:snippet_user_mentions) } - - let!(:snippet1) { snippets.create!(project_id: project.id, author_id: author.id, title: 'title1', description: description_mentions) } - let!(:snippet2) { snippets.create!(project_id: project.id, author_id: author.id, title: 'title2', description: 'some description') } - let!(:snippet3) { snippets.create!(project_id: project.id, author_id: author.id, title: 'title3', description: 'description with an email@example.com and some other @ char here.') } - - let(:user_mentions) { snippet_user_mentions } - let(:resource) { snippet1 } - - it_behaves_like 'resource mentions migration', MigrateSnippetMentionsToDb, Snippet - - context 'mentions in note' do - let!(:note1) { notes.create!(noteable_id: snippet1.id, noteable_type: 'Snippet', project_id: project.id, author_id: author.id, note: description_mentions) } - let!(:note2) { notes.create!(noteable_id: snippet1.id, noteable_type: 'Snippet', project_id: project.id, author_id: author.id, note: 'sample note') } - let!(:note3) { notes.create!(noteable_id: snippet1.id, noteable_type: 'Snippet', project_id: project.id, author_id: author.id, note: description_mentions, system: true) } - # this not does not have actual mentions - let!(:note4) { notes.create!(noteable_id: snippet1.id, noteable_type: 'Snippet', project_id: project.id, author_id: author.id, note: 'note3 for an email@somesite.com and some other rando @ ref' ) } - # this note points to an innexistent noteable record in snippets table - let!(:note5) { notes.create!(noteable_id: snippets.maximum(:id) + 10, noteable_type: 'Snippet', project_id: project.id, author_id: author.id, note: description_mentions) } - - it_behaves_like 'resource notes mentions migration', MigrateSnippetNotesMentionsToDb, Snippet - end - end - end - - context 'checks no_quote_columns' do - it 'has correct no_quote_columns' do - expect(Gitlab::BackgroundMigration::UserMentions::Models::Snippet.no_quote_columns).to match([:note_id, :snippet_id]) - end - end -end diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb index 2ece0dd4b5628a0136538d38206024626f051650..300ba66ee5ba806404211788a22ac3423678a32b 100644 --- a/spec/lib/gitlab/import_export/import_export_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_spec.rb @@ -21,4 +21,12 @@ describe Gitlab::ImportExport do expect(described_class.export_filename(exportable: project).length).to be < 70 end end + + describe '#snippet_repo_bundle_filename_for' do + let(:snippet) { build(:snippet, id: 1) } + + it 'generates the snippet bundle name' do + expect(described_class.snippet_repo_bundle_filename_for(snippet)).to eq "#{snippet.hexdigest}.bundle" + end + end end diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index 07857269004b683e8e8baf713b4cbc1eff1d4e0d..e03c95525dfbe395ec90f1c8f661e5ad7f36ee1f 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -50,7 +50,8 @@ describe Gitlab::ImportExport::Importer do Gitlab::ImportExport::WikiRestorer, Gitlab::ImportExport::UploadsRestorer, Gitlab::ImportExport::LfsRestorer, - Gitlab::ImportExport::StatisticsRestorer + Gitlab::ImportExport::StatisticsRestorer, + Gitlab::ImportExport::SnippetsRepoRestorer ].each do |restorer| it "calls the #{restorer}" do fake_restorer = double(restorer.to_s) diff --git a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d72d41ddf38596132f799aacedffe39456c5d76a --- /dev/null +++ b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::ImportExport::SnippetRepoRestorer do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, namespace: user.namespace) } + let(:snippet) { create(:project_snippet, project: project, author: user) } + + let(:shared) { project.import_export_shared } + let(:exporter) { Gitlab::ImportExport::SnippetsRepoSaver.new(project: project, shared: shared, current_user: user) } + let(:restorer) do + described_class.new(user: user, + shared: shared, + snippet: snippet, + path_to_bundle: snippet_bundle_path) + end + + after do + FileUtils.rm_rf(shared.export_path) + end + + shared_examples 'no bundle file present' do + it 'creates the repository from the database content' do + expect(snippet.repository_exists?).to be_falsey + + aggregate_failures do + expect(restorer.restore).to be_truthy + + expect(snippet.repository_exists?).to be_truthy + expect(snippet.snippet_repository).not_to be_nil + + blob = snippet.repository.blob_at('HEAD', snippet.file_name) + expect(blob).not_to be_nil + expect(blob.data).to eq(snippet.content) + end + end + end + + context 'when the snippet does not have a bundle file path' do + let(:snippet_bundle_path) { nil } + + it_behaves_like 'no bundle file present' + end + + context 'when the snippet bundle path is not present' do + let(:snippet_bundle_path) { 'foo' } + + it_behaves_like 'no bundle file present' + end + + context 'when the snippet bundle exists' do + let!(:snippet_with_repo) { create(:project_snippet, :repository, project: project) } + let(:bundle_path) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) } + let(:snippet_bundle_path) { File.join(bundle_path, "#{snippet_with_repo.hexdigest}.bundle") } + let(:result) { exporter.save } + + it 'creates the repository from the bundle' do + expect(exporter.save).to be_truthy + + expect(snippet.repository_exists?).to be_falsey + expect(snippet.snippet_repository).to be_nil + expect(snippet.repository).to receive(:create_from_bundle).and_call_original + + expect(restorer.restore).to be_truthy + expect(snippet.repository_exists?).to be_truthy + expect(snippet.snippet_repository).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb b/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7ad1ff213a1d929fb7deb4a01419a25635ece617 --- /dev/null +++ b/spec/lib/gitlab/import_export/snippet_repo_saver_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::ImportExport::SnippetRepoSaver do + describe 'bundle a project Git repo' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, namespace: user.namespace) } + let_it_be(:snippet) { create(:project_snippet, :repository, project: project, author: user) } + let(:shared) { project.import_export_shared } + let(:bundler) { described_class.new(project: project, shared: shared, repository: snippet.repository) } + let(:bundle_path) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) } + + around do |example| + FileUtils.mkdir_p(bundle_path) + example.run + ensure + FileUtils.rm_rf(bundle_path) + end + + context 'with project snippet' do + it 'bundles the repo successfully' do + aggregate_failures do + expect(bundler.save).to be_truthy + expect(Dir.empty?(bundle_path)).to be_falsey + end + end + + context 'when snippet does not have a repository' do + let(:snippet) { build(:personal_snippet) } + + it 'returns true' do + expect(bundler.save).to be_truthy + end + + it 'does not create any file' do + aggregate_failures do + expect(snippet.repository).not_to receive(:bundle_to_disk) + + bundler.save + + expect(Dir.empty?(bundle_path)).to be_truthy + end + end + end + end + end +end diff --git a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..242f6f6b58c1828898bc2a313d447b1d195a7e0d --- /dev/null +++ b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::ImportExport::SnippetsRepoRestorer do + include GitHelpers + + describe 'bundle a snippet Git repo' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, namespace: user.namespace) } + let_it_be(:snippet_with_repo) { create(:project_snippet, :repository, project: project, author: user) } + let_it_be(:snippet_without_repo) { create(:project_snippet, project: project, author: user) } + + let(:shared) { project.import_export_shared } + let(:exporter) { Gitlab::ImportExport::SnippetsRepoSaver.new(current_user: user, project: project, shared: shared) } + let(:bundle_dir) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) } + let(:restorer) do + described_class.new(user: user, + shared: shared, + project: project) + end + let(:service) { instance_double(Gitlab::ImportExport::SnippetRepoRestorer) } + + before do + exporter.save + end + + after do + FileUtils.rm_rf(shared.export_path) + end + + it 'calls SnippetRepoRestorer per each snippet with the bundle path' do + allow(service).to receive(:restore).and_return(true) + + expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet_with_repo, path_to_bundle: bundle_path(snippet_with_repo))).and_return(service) + expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet_without_repo, path_to_bundle: bundle_path(snippet_without_repo))).and_return(service) + + expect(restorer.restore).to be_truthy + end + + context 'when one snippet cannot be saved' do + it 'returns false and do not process other snippets' do + allow(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet_with_repo)).and_return(service) + allow(service).to receive(:restore).and_return(false) + + expect(Gitlab::ImportExport::SnippetRepoRestorer).not_to receive(:new).with(hash_including(snippet: snippet_without_repo)) + expect(restorer.restore).to be_falsey + end + end + + def bundle_path(snippet) + File.join(bundle_dir, ::Gitlab::ImportExport.snippet_repo_bundle_filename_for(snippet)) + end + end +end diff --git a/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb b/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5332990a97509f899a738842a88a60a4523ee9af --- /dev/null +++ b/spec/lib/gitlab/import_export/snippets_repo_saver_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::ImportExport::SnippetsRepoSaver do + describe 'bundle a project Git repo' do + let_it_be(:user) { create(:user) } + let!(:project) { create(:project) } + let(:shared) { project.import_export_shared } + let(:bundler) { described_class.new(current_user: user, project: project, shared: shared) } + + after do + FileUtils.rm_rf(shared.export_path) + end + + it 'creates the snippet bundles dir if not exists' do + snippets_dir = ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) + expect(Dir.exist?(snippets_dir)).to be_falsey + + bundler.save + + expect(Dir.exist?(snippets_dir)).to be_truthy + end + + context 'when project does not have any snippet' do + it 'does not perform any action' do + expect(Gitlab::ImportExport::SnippetRepoSaver).not_to receive(:new) + + bundler.save + end + end + + context 'when project has snippets' do + let!(:snippet1) { create(:project_snippet, :repository, project: project, author: user) } + let!(:snippet2) { create(:project_snippet, project: project, author: user) } + let(:service) { instance_double(Gitlab::ImportExport::SnippetRepoSaver) } + + it 'calls the SnippetRepoSaver for each snippet' do + allow(Gitlab::ImportExport::SnippetRepoSaver).to receive(:new).and_return(service) + expect(service).to receive(:save).and_return(true).twice + + bundler.save + end + + context 'when one snippet cannot be saved' do + it 'returns false and do not process other snippets' do + allow(Gitlab::ImportExport::SnippetRepoSaver).to receive(:new).with(hash_including(repository: snippet1.repository)).and_return(service) + allow(service).to receive(:save).and_return(false) + + expect(Gitlab::ImportExport::SnippetRepoSaver).not_to receive(:new).with(hash_including(repository: snippet2.repository)) + expect(bundler.save).to be_falsey + end + end + end + end +end diff --git a/spec/migrations/cleanup_empty_snippet_user_mentions_spec.rb b/spec/migrations/cleanup_empty_snippet_user_mentions_spec.rb deleted file mode 100644 index d229f0b2b59e58905d01b8fb72559f139483466c..0000000000000000000000000000000000000000 --- a/spec/migrations/cleanup_empty_snippet_user_mentions_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20200127111953_cleanup_empty_snippet_user_mentions') - -describe CleanupEmptySnippetUserMentions, :migration, :sidekiq do - let(:users) { table(:users) } - let(:projects) { table(:projects) } - let(:namespaces) { table(:namespaces) } - let(:snippets) { table(:snippets) } - let(:snippet_user_mentions) { table(:snippet_user_mentions) } - let(:notes) { table(:notes) } - - let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) } - let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id) } - let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) } - let(:snippet) { snippets.create!(title: "title1", title_html: 'title1', description: 'snippet description with @root mention', project_id: project.id, author_id: user.id) } - - let!(:resource1) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } - let!(:resource2) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet', system: true) } - let!(:resource3) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } - - # non-migrateable resources - # this note is already migrated, as it has a record in the snippet_user_mentions table - let!(:resource4) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } - let!(:user_mention) { snippet_user_mentions.create!(snippet_id: snippet.id, note_id: resource4.id, mentioned_users_ids: [1]) } - # this note points to an innexistent noteable record - let!(:resource5) { notes.create!(note: 'note for @root to check', noteable_id: snippets.maximum(:id) + 10, noteable_type: 'Snippet') } - - # these should get cleanup, by the migration - let!(:blank_snippet_user_mention1) { snippet_user_mentions.create!(snippet_id: snippet.id, note_id: resource1.id)} - let!(:blank_snippet_user_mention2) { snippet_user_mentions.create!(snippet_id: snippet.id, note_id: resource2.id)} - let!(:blank_snippet_user_mention3) { snippet_user_mentions.create!(snippet_id: snippet.id, note_id: resource3.id)} - - it 'cleanups blank user mentions' do - expect(snippet_user_mentions.count).to eq 4 - - migrate! - - expect(snippet_user_mentions.count).to eq 1 - end -end diff --git a/spec/migrations/migrate_snippet_mentions_to_db_spec.rb b/spec/migrations/migrate_snippet_mentions_to_db_spec.rb deleted file mode 100644 index 6644329fc113f8a62f6f1a32581c82fc2f5d5f92..0000000000000000000000000000000000000000 --- a/spec/migrations/migrate_snippet_mentions_to_db_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20200127131953_migrate_snippet_mentions_to_db') - -describe MigrateSnippetMentionsToDb, :migration, :sidekiq do - let(:users) { table(:users) } - let(:namespaces) { table(:namespaces) } - let(:projects) { table(:projects) } - let(:snippets) { table(:snippets) } - let(:snippet_user_mentions) { table(:snippet_user_mentions) } - - let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) } - let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id) } - let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) } - let!(:resource1) { snippets.create!(title: "title1", title_html: 'title1', description: 'snippet description with @root mention', project_id: project.id, author_id: user.id) } - let!(:resource2) { snippets.create!(title: "title2", title_html: "title2", description: 'snippet description with @group mention', project_id: project.id, author_id: user.id) } - let!(:resource3) { snippets.create!(title: "title3", title_html: "title3", description: 'snippet description with @project mention', project_id: project.id, author_id: user.id) } - - # non-migrateable resources - # this snippet is already migrated, as it has a record in the snippet_user_mentions table - let!(:resource4) { snippets.create!(title: "title4", title_html: "title4", description: 'snippet description with @project mention', project_id: project.id, author_id: user.id) } - let!(:user_mention) { snippet_user_mentions.create!(snippet_id: resource4.id, mentioned_users_ids: [1]) } - # this snippet has no mentions so should be filtered out - let!(:resource5) { snippets.create!(title: "title5", title_html: "title5", description: 'snippet description with no mention', project_id: project.id, author_id: user.id) } - - it_behaves_like 'schedules resource mentions migration', Snippet, false -end diff --git a/spec/migrations/migrate_snippet_notes_mentions_to_db_spec.rb b/spec/migrations/migrate_snippet_notes_mentions_to_db_spec.rb deleted file mode 100644 index 2ebe80e6ae3218cd7409ac40ea09aee7b41ee021..0000000000000000000000000000000000000000 --- a/spec/migrations/migrate_snippet_notes_mentions_to_db_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20200127151953_migrate_snippet_notes_mentions_to_db') - -describe MigrateSnippetNotesMentionsToDb, :migration, :sidekiq do - let(:users) { table(:users) } - let(:projects) { table(:projects) } - let(:namespaces) { table(:namespaces) } - let(:snippets) { table(:snippets) } - let(:snippet_user_mentions) { table(:snippet_user_mentions) } - let(:notes) { table(:notes) } - - let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) } - let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id) } - let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) } - let(:snippet) { snippets.create!(title: "title1", title_html: 'title1', description: 'snippet description with @root mention', project_id: project.id, author_id: user.id) } - - let!(:resource1) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } - let!(:resource2) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet', system: true) } - let!(:resource3) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } - - # non-migrateable resources - # this note is already migrated, as it has a record in the snippet_user_mentions table - let!(:resource4) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } - let!(:user_mention) { snippet_user_mentions.create!(snippet_id: snippet.id, note_id: resource4.id, mentioned_users_ids: [1]) } - # this note points to an innexistent noteable record - let!(:resource5) { notes.create!(note: 'note for @root to check', noteable_id: snippets.maximum(:id) + 10, noteable_type: 'Snippet') } - - it_behaves_like 'schedules resource mentions migration', Snippet, true -end diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb index 5b16b945efe4198733a9a1faa6b530ffc16c1460..120175fdd05711012d578add26ecfd19492aa93a 100644 --- a/spec/models/snippet_repository_spec.rb +++ b/spec/models/snippet_repository_spec.rb @@ -26,6 +26,44 @@ describe SnippetRepository do end end + describe '#create_file' do + let(:snippet) { create(:personal_snippet, :empty_repo, author: user) } + + it 'creates the file' do + snippet_repository.create_file(user, 'foo', 'bar', commit_opts) + blob = first_blob(snippet) + + aggregate_failures do + expect(blob).not_to be_nil + expect(blob.path).to eq 'foo' + expect(blob.data).to eq 'bar' + end + end + + it 'fills the file path if empty' do + snippet_repository.create_file(user, nil, 'bar', commit_opts) + blob = first_blob(snippet) + + aggregate_failures do + expect(blob).not_to be_nil + expect(blob.path).to eq 'snippetfile1.txt' + expect(blob.data).to eq 'bar' + end + end + + context 'when the file exists' do + let(:snippet) { create(:personal_snippet, :repository, author: user) } + + it 'captures the git exception and raises a SnippetRepository::CommitError' do + existing_blob = first_blob(snippet) + + expect do + snippet_repository.create_file(user, existing_blob.path, existing_blob.data, commit_opts) + end.to raise_error described_class::CommitError + end + end + end + describe '#multi_files_action' do let(:new_file) { { file_path: 'new_file_test', content: 'bar' } } let(:move_file) { { previous_path: 'CHANGELOG', file_path: 'CHANGELOG_new', content: 'bar' } } diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb index 4d51deaa404f6f02c4b247c77f02d17ace8d0b11..e00507d1827a3b8a3530bbc305930fe6862731f3 100644 --- a/spec/services/projects/import_export/export_service_spec.rb +++ b/spec/services/projects/import_export/export_service_spec.rb @@ -64,6 +64,14 @@ describe Projects::ImportExport::ExportService do service.execute end + it 'saves the snippets' do + expect_next_instance_of(Gitlab::ImportExport::SnippetsRepoSaver) do |instance| + expect(instance).to receive(:save).and_call_original + end + + service.execute + end + context 'when all saver services succeed' do before do allow(service).to receive(:save_services).and_return(true) diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb index f5b889ef720a5e600df054e00de8bce4d0a1da4a..8f5bfdacc3afeda949fc5ede61d35b33aa4a7a12 100644 --- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb @@ -72,7 +72,7 @@ shared_examples 'schedules resource mentions migration' do |resource_class, is_f it 'schedules background migrations' do Sidekiq::Testing.fake! do Timecop.freeze do - resource_count = is_for_notes ? Note.where(noteable_type: resource_class.to_s).count : resource_class.count + resource_count = is_for_notes ? Note.count : resource_class.count expect(resource_count).to eq 5 migrate!