From 9a73b634ab4220f68a8296ccb582a68293874489 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 9 Jun 2017 12:48:25 +0100 Subject: [PATCH] Add table for files in merge request diffs This adds an ID-less table containing one row per file, per merge request diff. It has a column for each attribute on Gitlab::Git::Diff that is serialised currently, with the advantage that we can easily query the attributes of this new table. It does not migrate existing data, so we have fallback code when the legacy st_diffs column is present instead. For a merge request diff to be valid, it should have at most one of: * Rows in this new table, with the correct merge_request_diff_id. * A non-NULL st_diffs column. It may have neither, if the diff is empty. --- app/models/merge_request_diff.rb | 54 ++++++++++++------ app/models/merge_request_diff_file.rb | 11 ++++ ...8171156_create_merge_request_diff_files.rb | 22 +++++++ ...merge_request_diff_file_limits_to_mysql.rb | 1 + ...merge_request_diff_file_limits_to_mysql.rb | 12 ++++ db/schema.rb | 19 +++++- doc/user/project/settings/import_export.md | 17 +++--- lib/gitlab/database.rb | 16 ++++++ lib/gitlab/git/diff.rb | 17 +++--- lib/gitlab/import_export.rb | 2 +- lib/gitlab/import_export/import_export.yml | 7 ++- lib/gitlab/import_export/json_hash_builder.rb | 4 +- lib/gitlab/import_export/relation_factory.rb | 5 ++ lib/tasks/migrate/add_limits_mysql.rake | 2 + .../import_export/test_project_export.tar.gz | Bin 681478 -> 681481 bytes spec/lib/gitlab/database_spec.rb | 49 ++++++++++++++++ spec/lib/gitlab/git/diff_spec.rb | 2 +- spec/lib/gitlab/import_export/all_models.yml | 3 + spec/lib/gitlab/import_export/project.json | 38 ++++++++---- .../project_tree_restorer_spec.rb | 7 ++- .../import_export/project_tree_saver_spec.rb | 10 ++++ .../import_export/safe_model_attributes.yml | 11 ++++ spec/models/merge_request_diff_file_spec.rb | 11 ++++ spec/models/merge_request_diff_spec.rb | 3 +- 24 files changed, 271 insertions(+), 52 deletions(-) create mode 100644 app/models/merge_request_diff_file.rb create mode 100644 db/migrate/20170608171156_create_merge_request_diff_files.rb create mode 100644 db/migrate/20170614115405_merge_request_diff_file_limits_to_mysql.rb create mode 100644 db/migrate/merge_request_diff_file_limits_to_mysql.rb create mode 100644 spec/models/merge_request_diff_file_spec.rb diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 99dd2130188..f1ee4d3f7a9 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -10,6 +10,7 @@ class MergeRequestDiff < ActiveRecord::Base VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze belongs_to :merge_request + has_many :merge_request_diff_files, -> { order(:merge_request_diff_id, :relative_order) } serialize :st_commits # rubocop:disable Cop/ActiverecordSerialize serialize :st_diffs # rubocop:disable Cop/ActiverecordSerialize @@ -91,7 +92,7 @@ class MergeRequestDiff < ActiveRecord::Base head_commit_sha).diffs(options) else @raw_diffs ||= {} - @raw_diffs[options] ||= load_diffs(st_diffs, options) + @raw_diffs[options] ||= load_diffs(options) end end @@ -253,24 +254,44 @@ class MergeRequestDiff < ActiveRecord::Base update_columns_serialized(new_attributes) end - def dump_diffs(diffs) - if diffs.respond_to?(:map) - diffs.map(&:to_hash) + def create_merge_request_diff_files(diffs) + rows = diffs.map.with_index do |diff, index| + diff.to_hash.merge( + merge_request_diff_id: self.id, + relative_order: index + ) end + + Gitlab::Database.bulk_insert('merge_request_diff_files', rows) end - def load_diffs(raw, options) - if valid_raw_diff?(raw) - if paths = options[:paths] - raw = raw.select do |diff| - paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) - end - end + def load_diffs(options) + return Gitlab::Git::DiffCollection.new([]) unless diffs_from_database - Gitlab::Git::DiffCollection.new(raw, options) - else - Gitlab::Git::DiffCollection.new([]) + raw = diffs_from_database + + if paths = options[:paths] + raw = raw.select do |diff| + paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) + end end + + Gitlab::Git::DiffCollection.new(raw, options) + end + + def diffs_from_database + return @diffs_from_database if defined?(@diffs_from_database) + + @diffs_from_database = + if st_diffs.present? + if valid_raw_diff?(st_diffs) + st_diffs + end + elsif merge_request_diff_files.present? + merge_request_diff_files + .as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS) + .map(&:with_indifferent_access) + end end # Load diffs between branches related to current merge request diff from repo @@ -285,11 +306,10 @@ class MergeRequestDiff < ActiveRecord::Base new_attributes[:real_size] = diff_collection.real_size if diff_collection.any? - new_diffs = dump_diffs(diff_collection) new_attributes[:state] = :collected - end - new_attributes[:st_diffs] = new_diffs || [] + create_merge_request_diff_files(diff_collection) + end # Set our state to 'overflow' to make the #empty? and #collected? # methods (generated by StateMachine) return false. diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb new file mode 100644 index 00000000000..598ebd4d829 --- /dev/null +++ b/app/models/merge_request_diff_file.rb @@ -0,0 +1,11 @@ +class MergeRequestDiffFile < ActiveRecord::Base + include Gitlab::EncodingHelper + + belongs_to :merge_request_diff + + def utf8_diff + return '' if diff.blank? + + encode_utf8(diff) if diff.respond_to?(:encoding) + end +end diff --git a/db/migrate/20170608171156_create_merge_request_diff_files.rb b/db/migrate/20170608171156_create_merge_request_diff_files.rb new file mode 100644 index 00000000000..bf0c0d29adc --- /dev/null +++ b/db/migrate/20170608171156_create_merge_request_diff_files.rb @@ -0,0 +1,22 @@ +class CreateMergeRequestDiffFiles < ActiveRecord::Migration + DOWNTIME = false + + disable_ddl_transaction! + + def change + create_table :merge_request_diff_files, id: false do |t| + t.belongs_to :merge_request_diff, null: false, foreign_key: { on_delete: :cascade } + t.integer :relative_order, null: false + t.boolean :new_file, null: false + t.boolean :renamed_file, null: false + t.boolean :deleted_file, null: false + t.boolean :too_large, null: false + t.string :a_mode, null: false + t.string :b_mode, null: false + t.text :new_path, null: false + t.text :old_path, null: false + t.text :diff, null: false + t.index [:merge_request_diff_id, :relative_order], name: 'index_merge_request_diff_files_on_mr_diff_id_and_order', unique: true + end + end +end diff --git a/db/migrate/20170614115405_merge_request_diff_file_limits_to_mysql.rb b/db/migrate/20170614115405_merge_request_diff_file_limits_to_mysql.rb new file mode 100644 index 00000000000..4c1cf08aa06 --- /dev/null +++ b/db/migrate/20170614115405_merge_request_diff_file_limits_to_mysql.rb @@ -0,0 +1 @@ +require_relative 'merge_request_diff_file_limits_to_mysql' diff --git a/db/migrate/merge_request_diff_file_limits_to_mysql.rb b/db/migrate/merge_request_diff_file_limits_to_mysql.rb new file mode 100644 index 00000000000..3958380e4b9 --- /dev/null +++ b/db/migrate/merge_request_diff_file_limits_to_mysql.rb @@ -0,0 +1,12 @@ +class MergeRequestDiffFileLimitsToMysql < ActiveRecord::Migration + DOWNTIME = false + + def up + return unless Gitlab::Database.mysql? + + change_column :merge_request_diff_files, :diff, :text, limit: 2147483647 + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 803e36fba5a..f42827991aa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170609183112) do +ActiveRecord::Schema.define(version: 20170614115405) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -692,6 +692,22 @@ ActiveRecord::Schema.define(version: 20170609183112) do add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree + create_table "merge_request_diff_files", id: false, force: :cascade do |t| + t.integer "merge_request_diff_id", null: false + t.integer "relative_order", null: false + t.boolean "new_file", null: false + t.boolean "renamed_file", null: false + t.boolean "deleted_file", null: false + t.boolean "too_large", null: false + t.string "a_mode", null: false + t.string "b_mode", null: false + t.text "new_path", null: false + t.text "old_path", null: false + t.text "diff", null: false + end + + add_index "merge_request_diff_files", ["merge_request_diff_id", "relative_order"], name: "index_merge_request_diff_files_on_mr_diff_id_and_order", unique: true, using: :btree + create_table "merge_request_diffs", force: :cascade do |t| t.string "state" t.text "st_commits" @@ -1530,6 +1546,7 @@ ActiveRecord::Schema.define(version: 20170609183112) do add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "lists", "boards" add_foreign_key "lists", "labels" + add_foreign_key "merge_request_diff_files", "merge_request_diffs", on_delete: :cascade add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 58d2fd76c61..35960ade3d4 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -27,14 +27,15 @@ with all their related data and be moved into a new GitLab instance. | GitLab version | Import/Export version | | -------- | -------- | -| 9.2.0 to current | 0.1.7 | -| 8.17.0 | 0.1.6 | -| 8.13.0 | 0.1.5 | -| 8.12.0 | 0.1.4 | -| 8.10.3 | 0.1.3 | -| 8.10.0 | 0.1.2 | -| 8.9.5 | 0.1.1 | -| 8.9.0 | 0.1.0 | +| 9.4.0 to current | 0.1.8 | +| 9.2.0 | 0.1.7 | +| 8.17.0 | 0.1.6 | +| 8.13.0 | 0.1.5 | +| 8.12.0 | 0.1.4 | +| 8.10.3 | 0.1.3 | +| 8.10.0 | 0.1.2 | +| 8.9.5 | 0.1.1 | +| 8.9.0 | 0.1.0 | > The table reflects what GitLab version we updated the Import/Export version at. > For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3) diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index d0bd1299671..0d5a7cf0694 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -83,6 +83,22 @@ module Gitlab end end + def self.bulk_insert(table, rows) + return if rows.empty? + + keys = rows.first.keys + columns = keys.map { |key| connection.quote_column_name(key) } + + tuples = rows.map do |row| + row.values_at(*keys).map { |value| connection.quote(value) } + end + + connection.execute <<-EOF.strip_heredoc + INSERT INTO #{table} (#{columns.join(', ')}) + VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')} + EOF + end + # pool_size - The size of the DB pool. # host - An optional host name to use instead of the default one. def self.create_connection_pool(pool_size, host = nil) diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index 4b689f0e94f..f825568f194 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -16,11 +16,11 @@ module Gitlab alias_method :renamed_file?, :renamed_file attr_accessor :expanded + attr_writer :too_large alias_method :expanded?, :expanded - # We need this accessor because of `to_hash` and `init_from_hash` - attr_accessor :too_large + SERIALIZE_KEYS = %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large).freeze class << self # The maximum size of a diff to display. @@ -231,16 +231,10 @@ module Gitlab end end - def serialize_keys - @serialize_keys ||= %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large) - end - def to_hash hash = {} - keys = serialize_keys - - keys.each do |key| + SERIALIZE_KEYS.each do |key| hash[key] = send(key) end @@ -267,6 +261,9 @@ module Gitlab end end + # This is used by `to_hash` and `init_from_hash`. + alias_method :too_large, :too_large? + def too_large! @diff = '' @line_count = 0 @@ -315,7 +312,7 @@ module Gitlab def init_from_hash(hash) raw_diff = hash.symbolize_keys - serialize_keys.each do |key| + SERIALIZE_KEYS.each do |key| send(:"#{key}=", raw_diff[key.to_sym]) end end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 27d5a9198b6..3470a09eaf0 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -3,7 +3,7 @@ module Gitlab extend self # For every version update, the version history in import_export.md has to be kept up to date. - VERSION = '0.1.7'.freeze + VERSION = '0.1.8'.freeze FILENAME_LIMIT = 50 def export_path(relative_path:) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index ff2b1d08c3c..72183e8aad4 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -26,7 +26,8 @@ project_tree: - notes: - :author - :events - - :merge_request_diff + - merge_request_diff: + - :merge_request_diff_files - :events - :timelogs - label_links: @@ -92,6 +93,8 @@ excluded_attributes: - :expired_at merge_request_diff: - :st_diffs + merge_request_diff_files: + - :diff issues: - :milestone_id merge_requests: @@ -113,6 +116,8 @@ methods: - :type merge_request_diff: - :utf8_st_diffs + merge_request_diff_files: + - :utf8_diff merge_requests: - :diff_head_sha project: diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb index 48c09dafcb6..b48f63bcd7e 100644 --- a/lib/gitlab/import_export/json_hash_builder.rb +++ b/lib/gitlab/import_export/json_hash_builder.rb @@ -83,7 +83,9 @@ module Gitlab # +value+ existing model to be included in the hash # +json_config_hash+ the original hash containing the root model def add_model_value(current_key, value, json_config_hash) - @attributes_finder.parse(value) { |hash| value = { value => hash } } + @attributes_finder.parse(value) do |hash| + value = { value => hash } unless value.is_a?(Hash) + end add_to_array(current_key, json_config_hash, value) end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 695852526cb..20580459046 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -71,6 +71,7 @@ module Gitlab @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data'] set_st_diff_commits if @relation_name == :merge_request_diff + set_diff if @relation_name == :merge_request_diff_files end def update_user_references @@ -202,6 +203,10 @@ module Gitlab HashUtil.deep_symbolize_array_with_date!(@relation_hash['st_commits']) end + def set_diff + @relation_hash['diff'] = @relation_hash.delete('utf8_diff') + end + def existing_or_new_object # Only find existing records to avoid mapping tables such as milestones # Otherwise always create the record, skipping the extra SELECT clause. diff --git a/lib/tasks/migrate/add_limits_mysql.rake b/lib/tasks/migrate/add_limits_mysql.rake index 761f275d42a..151f42a2222 100644 --- a/lib/tasks/migrate/add_limits_mysql.rake +++ b/lib/tasks/migrate/add_limits_mysql.rake @@ -1,9 +1,11 @@ require Rails.root.join('db/migrate/limits_to_mysql') require Rails.root.join('db/migrate/markdown_cache_limits_to_mysql') +require Rails.root.join('db/migrate/merge_request_diff_file_limits_to_mysql') desc "GitLab | Add limits to strings in mysql database" task add_limits_mysql: :environment do puts "Adding limits to schema.rb for mysql" LimitsToMysql.new.up MarkdownCacheLimitsToMysql.new.up + MergeRequestDiffFileLimitsToMysql.new.up end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz index 4efd5a26a823ea7a93d6e1c1ce379a05b6171d47..e03e7b881742aab75ca001d26f5c5d4c14c1e040 100644 GIT binary patch delta 18960 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 diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 26e5d73d333..428b6edb7d6 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -129,6 +129,55 @@ describe Gitlab::Database, lib: true do end end + describe '.bulk_insert' do + before do + allow(described_class).to receive(:connection).and_return(connection) + allow(connection).to receive(:quote_column_name, &:itself) + allow(connection).to receive(:quote, &:itself) + allow(connection).to receive(:execute) + end + + let(:connection) { double(:connection) } + + let(:rows) do + [ + { a: 1, b: 2, c: 3 }, + { c: 6, a: 4, b: 5 } + ] + end + + it 'does nothing with empty rows' do + expect(connection).not_to receive(:execute) + + described_class.bulk_insert('test', []) + end + + it 'uses the ordering from the first row' do + expect(connection).to receive(:execute) do |sql| + expect(sql).to include('(1, 2, 3)') + expect(sql).to include('(4, 5, 6)') + end + + described_class.bulk_insert('test', rows) + end + + it 'quotes column names' do + expect(connection).to receive(:quote_column_name).with(:a) + expect(connection).to receive(:quote_column_name).with(:b) + expect(connection).to receive(:quote_column_name).with(:c) + + described_class.bulk_insert('test', rows) + end + + it 'quotes values' do + 1.upto(6) do |i| + expect(connection).to receive(:quote).with(i) + end + + described_class.bulk_insert('test', rows) + end + end + describe '.create_connection_pool' do it 'creates a new connection pool with specific pool size' do pool = described_class.create_connection_pool(5) diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index da213f617cc..78d741f0110 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -90,7 +90,7 @@ EOT let(:diff) { described_class.new(@rugged_diff) } it 'initializes the diff' do - expect(diff.to_hash).to eq(@raw_diff_hash.merge(too_large: nil)) + expect(diff.to_hash).to eq(@raw_diff_hash) end it 'does not prune the diff' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 412eb33b35b..a5f09f1856e 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -88,6 +88,9 @@ merge_requests: - head_pipeline merge_request_diff: - merge_request +- merge_request_diff_files +merge_request_diff_files: +- merge_request_diff pipelines: - project - user diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index e3599d6fe59..98c117b4cd8 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2821,9 +2821,11 @@ "committer_email": "dmitriy.zaporozhets@gmail.com" } ], - "utf8_st_diffs": [ + "merge_request_diff_files": [ { - "diff": "Binary files a/.DS_Store and /dev/null differ\n", + "merge_request_diff_id": 27, + "relative_order": 0, + "utf8_diff": "Binary files a/.DS_Store and /dev/null differ\n", "new_path": ".DS_Store", "old_path": ".DS_Store", "a_mode": "100644", @@ -2834,7 +2836,9 @@ "too_large": false }, { - "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", + "merge_request_diff_id": 27, + "relative_order": 1, + "utf8_diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", "new_path": ".gitignore", "old_path": ".gitignore", "a_mode": "100644", @@ -2845,7 +2849,9 @@ "too_large": false }, { - "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", + "merge_request_diff_id": 27, + "relative_order": 2, + "utf8_diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", "new_path": ".gitmodules", "old_path": ".gitmodules", "a_mode": "100644", @@ -2856,7 +2862,9 @@ "too_large": false }, { - "diff": "Binary files a/files/.DS_Store and /dev/null differ\n", + "merge_request_diff_id": 27, + "relative_order": 3, + "utf8_diff": "Binary files a/files/.DS_Store and /dev/null differ\n", "new_path": "files/.DS_Store", "old_path": "files/.DS_Store", "a_mode": "100644", @@ -2867,7 +2875,9 @@ "too_large": false }, { - "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n", + "merge_request_diff_id": 27, + "relative_order": 4, + "utf8_diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n", "new_path": "files/ruby/feature.rb", "old_path": "files/ruby/feature.rb", "a_mode": "0", @@ -2878,7 +2888,9 @@ "too_large": false }, { - "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", + "merge_request_diff_id": 27, + "relative_order": 5, + "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", "new_path": "files/ruby/popen.rb", "old_path": "files/ruby/popen.rb", "a_mode": "100644", @@ -2889,7 +2901,9 @@ "too_large": false }, { - "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", + "merge_request_diff_id": 27, + "relative_order": 6, + "utf8_diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", "new_path": "files/ruby/regex.rb", "old_path": "files/ruby/regex.rb", "a_mode": "100644", @@ -2900,7 +2914,9 @@ "too_large": false }, { - "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", + "merge_request_diff_id": 27, + "relative_order": 7, + "utf8_diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", "new_path": "gitlab-grack", "old_path": "gitlab-grack", "a_mode": "0", @@ -2911,7 +2927,9 @@ "too_large": false }, { - "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", + "merge_request_diff_id": 27, + "relative_order": 8, + "utf8_diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", "new_path": "gitlab-shell", "old_path": "gitlab-shell", "a_mode": "0", diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 14338515892..c11b15a811b 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -86,8 +86,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do it 'has the correct data for merge request st_diffs' do # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+ + # one MergeRequestDiff uses the new format, where st_diffs is expected to be nil - expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9) + expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(8) + end + + it 'has the correct data for merge request diff files' do + expect(MergeRequestDiffFile.where.not(diff: nil).count).to eq(9) end it 'has the correct time for merge request st_commits' do diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 5aeb29b7fec..e52f79513f1 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -83,6 +83,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['merge_requests'].first['merge_request_diff']['utf8_st_diffs']).not_to be_nil end + it 'has merge request diff files' do + expect(saved_project_json['merge_requests'].first['merge_request_diff']['merge_request_diff_files']).not_to be_empty + end + it 'has merge requests comments' do expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty end @@ -145,6 +149,12 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(project_tree_saver.save).to be true end + it 'does not complain about non UTF-8 characters in MR diff files' do + ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") + + expect(project_tree_saver.save).to be true + end + context 'group members' do let(:user2) { create(:user, email: 'group@member.com') } let(:member_emails) do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 50ff6ecc1e0..fadd3ad1330 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -172,6 +172,17 @@ MergeRequestDiff: - real_size - head_commit_sha - start_commit_sha +MergeRequestDiffFile: +- merge_request_diff_id +- relative_order +- new_file +- renamed_file +- deleted_file +- new_path +- old_path +- a_mode +- b_mode +- too_large Ci::Pipeline: - id - project_id diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb new file mode 100644 index 00000000000..7276f5b5061 --- /dev/null +++ b/spec/models/merge_request_diff_file_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +describe MergeRequestDiffFile, type: :model do + describe '#utf8_diff' do + it 'does not raise error when a hash value is in binary' do + subject.diff = "\x05\x00\x68\x65\x6c\x6c\x6f" + + expect { subject.utf8_diff }.not_to raise_error + end + end +end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 25f7062860b..4ad4abaa572 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -37,7 +37,7 @@ describe MergeRequestDiff, models: true do context 'when the raw diffs are empty' do before do - mr_diff.update_attributes(st_diffs: '') + MergeRequestDiffFile.delete_all(merge_request_diff_id: mr_diff.id) end it 'returns an empty DiffCollection' do @@ -48,6 +48,7 @@ describe MergeRequestDiff, models: true do context 'when the raw diffs have invalid content' do before do + MergeRequestDiffFile.delete_all(merge_request_diff_id: mr_diff.id) mr_diff.update_attributes(st_diffs: ["--broken-diff"]) end