Wiki 依存の排除 (#431) (#432)

Reviewed-on: #432
Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #432 でマージされました。
このコミットが含まれているのは:
2026-10-16 00:26:53 +09:00
committed by みてるぞ
コミット 75d7dac999
75個のファイルの変更、1474行の追加、3000行の削除
+12 -15
ファイルの表示
@@ -95,14 +95,10 @@ class PostCreatePlan
def build_default_tag_specs locale, direct_tag_specs
default_tag_specs = []
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
default_tag_specs << {
name: TAGME_TAG_NAME,
category: :meta }
default_tag_specs << { name: TAGME_TAG_NAME, category: :meta }
end
if direct_tag_specs.none? { deerjikist_tag_spec?(locale, _1) }
default_tag_specs << {
name: NO_DEERJIKIST_TAG_NAME,
category: :meta }
default_tag_specs << { name: NO_DEERJIKIST_TAG_NAME, category: :meta }
end
default_tag_specs
@@ -117,9 +113,9 @@ class PostCreatePlan
end
def validate_new_tag_spec! locale, spec
tag_name = TagName.new(name: spec[:name], language_code: locale.language_code,
script_code: locale.script_code, primary_flg: true)
tag = Tag.new(category: spec[:category])
tag_name = TagName.new(tag:, name: spec[:name], language_code: locale.language_code,
script_code: locale.script_code, primary_flg: true)
return if tag_name.valid? && tag.valid?
post = Post.new
@@ -140,10 +136,10 @@ class PostCreatePlan
end
expanded_parent_specs =
Tag.expand_parent_tags(existing_snapshot_tags)
.reject(&:deprecated?)
.map { |tag|
{ name: tag.name(locale.language_code),
category: tag.category.to_sym }
.reject(&:deprecated?)
.map { |tag|
{ name: tag.name(locale.language_code),
category: tag.category.to_sym }
}
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
end
@@ -164,9 +160,10 @@ class PostCreatePlan
def existing_tags_by_name locale
@existing_tags_by_name ||= begin
names = tag_names.map { canonical_tag_name_without_sections(locale, _1) }.uniq
TagName.where(language_code: locale.language_code, primary_flg: true, name: names)
.includes(:tag)
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
TagName
.where(language_code: locale.language_code, primary_flg: true, name: names)
.includes(:tag)
.each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag }
end
end
+44 -9
ファイルの表示
@@ -4,7 +4,14 @@ require 'digest'
class SharedResourceIdAllocator
SPACES = {
'tags' => ['tags', 'external_tags'],
'wiki' => ['tag_descriptions', 'tag_groups'] }.freeze
'wiki' => ['tag_descriptions', 'tag_groups'],
'wiki_history' => ['tag_description_versions', 'tag_group_versions'] }.freeze
HISTORY_IDS = {
'tags' => [
['tag_versions', 'tag_id'],
['nico_tag_versions', 'tag_id'],
['tag_description_versions', 'tag_id']],
'wiki' => [['tag_group_versions', 'tag_group_id']] }.freeze
LOCK_TIMEOUT = 10
def self.assign! record
@@ -13,7 +20,9 @@ class SharedResourceIdAllocator
connection = record.class.connection
transaction = record.class.current_transaction
raise ActiveRecord::ActiveRecordError, 'Shared ID requires a transaction' unless transaction.open?
unless transaction.open?
raise ActiveRecord::ActiveRecordError, 'Shared ID requires a transaction'
end
database = connection.pool.db_config.database
name = "btrc:ids:#{ Digest::SHA256.hexdigest(database)[0, 16] }:#{ space }"
@@ -41,16 +50,42 @@ class SharedResourceIdAllocator
raise
end
# 明示-tic な Id. 指定は許可する.
return if record.id.present?
# Locking reads see committed maxima even after earlier repeatable reads.
maxima = record.class.uncached do
tables.map do |table|
next_id = record.class.uncached do
historical_max = HISTORY_IDS.fetch(space, []).map { |table, column|
quoted_table = connection.quote_table_name(table)
connection.select_value(
"SELECT id FROM #{ quoted_table } ORDER BY id DESC LIMIT 1 FOR UPDATE").to_i
end
quoted_column = connection.quote_column_name(column)
connection.select_value("SELECT MAX(#{ quoted_column }) FROM #{ quoted_table }").to_i
}.max.to_i
[next_ids(connection, tables).max, historical_max + 1].max
end
record.id = maxima.max + 1
record.id = next_id
end
def self.next_ids connection, tables
expiry = connection.select_value('SELECT @@SESSION.information_schema_stats_expiry').to_i
begin
connection.execute('SET SESSION information_schema_stats_expiry = 0')
quoted_tables = tables.map { connection.quote(_1) }.join(', ')
rows = connection.select_all(<<~SQL).to_a
SELECT TABLE_NAME, ENGINE, AUTO_INCREMENT
FROM information_schema.tables
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (#{ quoted_tables })
SQL
persistent = rows.all? do |row|
row['ENGINE'] == 'InnoDB' && row['AUTO_INCREMENT'].to_i.positive?
end
unless rows.size == tables.size && persistent
raise ActiveRecord::ActiveRecordError, 'Persistent InnoDB counters are required'
end
rows.map { _1.fetch('AUTO_INCREMENT').to_i }
ensure
connection.execute("SET SESSION information_schema_stats_expiry = #{ expiry }")
end
end
private_class_method :next_ids
end
+46 -51
ファイルの表示
@@ -1,72 +1,67 @@
class TagDescriptionOperation
LANGUAGE_CODE = 'ja'
class Conflict < StandardError
;
end
def self.create!(tag:, body:, created_by_user:, message: nil)
def self.create! locale, tag:, body:, created_by_user:, message: nil
ApplicationRecord.transaction do
tag.lock!
raise ActiveRecord::RecordNotUnique if TagDescription.exists?(
tag_id: tag.id, language_code: LANGUAGE_CODE)
tag_id: tag.id, language_code: locale.language_code)
page = Wiki::CompatibilityWriter.create!(
tag_name: compatibility_tag_name(tag), body:, created_by_user:, message:)
revision = page.wiki_revisions.order(id: :desc).first
record_version!(tag:, revision:, body: revision.body, user: created_by_user,
description: nil)
page
description = TagDescription.new(tag:, language_code: locale.language_code)
body = normalise_body!(description, body)
record_version!(description:, body:, user: created_by_user)
description
end
end
def self.update!(tag:, body:, updated_by_user:, message: nil,
base_revision_id: nil)
def self.update! locale, tag:, body:, updated_by_user:, message: nil, base_revision_id: nil
ApplicationRecord.transaction do
tag.lock!
description = TagDescription.lock.find_by!(
tag_id: tag.id, language_code: LANGUAGE_CODE)
tag_name = compatibility_tag_name(tag)
page = WikiPage.lock.find_by(tag_name_id: tag_name.id)
unless page
versions = TagDescriptionVersion.where(tag_id: tag.id, language_code: LANGUAGE_CODE)
if base_revision_id.present? && versions.maximum(:id) != base_revision_id.to_i
raise Wiki::Commit::Conflict, '競合が発生してゐます.'
end
page = Wiki::CompatibilityWriter.create!(
tag_name:, body:, created_by_user: updated_by_user, message:)
revision = page.current_revision
record_version!(tag:, revision:, body: revision.body,
user: updated_by_user, description:)
next revision
tag_id: tag.id, language_code: locale.language_code)
body = normalise_body!(description, body)
latest = TagDescriptionVersion.find_by(
tag_id: tag.id, language_code: locale.language_code,
version_no: description.version_no)
if base_revision_id.present? && latest&.id != base_revision_id.to_i
raise Conflict, '競合が発生してゐます.'
end
revision = Wiki::CompatibilityWriter.update!(
page:, body:, updated_by_user:, message:,
base_revision_id:)
next unless revision
next if latest && normalise_body(description.body) == body
record_version!(tag:, revision:, body: revision.body,
user: updated_by_user, description:)
revision
record_version!(description:, body:, user: updated_by_user)
end
end
def self.compatibility_tag_name tag
tag.tag_name(LANGUAGE_CODE) || TagName.find_by!(id: tag.tag_name_id,
language_code: LANGUAGE_CODE)
end
def self.record_version! tag:, revision:, body:, user:, description:
version_no = description ? description.version_no + 1 : 1
def self.record_version! description:, body:, user:
creating = description.new_record?
version_no = creating ? 1 : description.version_no + 1
description.created_by_user_id = user.id if creating
description.update!(body:, version_no:, updated_by_user_id: user.id)
TagDescriptionVersion.create!(
id: revision.id, tag_id: tag.id, language_code: LANGUAGE_CODE,
version_no:, event_type: description ? 'update' : 'create', body:,
created_by_user_id: user.id, created_at: revision.created_at)
if description
description.update!(body:, version_no:, updated_by_user_id: user.id)
else
TagDescription.create!(
tag_id: tag.id,
language_code: LANGUAGE_CODE, version_no:, body:,
created_by_user_id: user.id, updated_by_user_id: user.id)
end
tag_id: description.tag_id, language_code: description.language_code,
version_no:, event_type: creating ? 'create' : 'update', body:,
source_language_code: description.source_language_code,
source_version_no: description.source_version_no,
created_by_user_id: user.id, created_at: Time.current)
end
private_class_method :record_version!
def self.normalise_body body
body.to_s.gsub(/\r\n?/, "\n")
.encode('UTF-8', invalid: :replace, undef: :replace, replace: '🖕')
.sub(/\n+\z/, '')
end
private_class_method :normalise_body
def self.normalise_body! description, body
normalised = normalise_body(body)
return normalised if normalised.present?
description.errors.add(:body, :blank)
raise ActiveRecord::RecordInvalid, description
end
private_class_method :normalise_body!
end
+22 -21
ファイルの表示
@@ -7,8 +7,8 @@ class TagGroupMarkdownParser
;
end
def self.parse(body:, group: nil)
new(body:, group:).parse
def self.parse(locale, body:, group: nil)
new(body:, group:).parse(locale)
end
def initialize(body:, group:)
@@ -16,7 +16,7 @@ class TagGroupMarkdownParser
@group = group
end
def parse
def parse locale
parse_body = @body.gsub(/([^\n])\n([\x23]{1,6}[ \t]+\S)/, "\\1\n\n\\2")
root = Kramdown::Document.new(parse_body, input: 'GFM', auto_ids: false).root
placements = []
@@ -51,7 +51,7 @@ class TagGroupMarkdownParser
when :ul
invalid!('未対応のリスト項目があります.') if node.attr.any?
target = headings.any? ? headings.last.fetch(:node).fetch('children') : placements
target.concat(node.children.map { parse_list_item(_1) })
target.concat(node.children.map { parse_list_item(locale, _1) })
else
invalid!("未対応の Markdown 構文 #{ node.type.inspect } です.")
end
@@ -66,7 +66,7 @@ class TagGroupMarkdownParser
private
def parse_list_item(item)
def parse_list_item locale, item
invalid!('リスト項目ではありません.') unless item.type == :li
invalid!('未対応のリスト項目があります.') if item.attr.any?
@@ -81,29 +81,30 @@ class TagGroupMarkdownParser
label = own.map { inline_text(_1) }.join(' ').strip
invalid!('空のリスト項目は使用できません.') if label.blank?
node = if label.start_with?(PREFIX)
group_name = label.delete_prefix(PREFIX).strip
child_group = TagGroup.find_by(name: group_name)
invalid!("Tag Group #{ group_name.inspect } を解決できません.") unless child_group
invalid!('自分自身を含めることはできません.') if child_group.id == @group&.id
{ 'tag_group_id' => child_group.id }
elsif label.end_with?(':', ':')
section_name = label.sub(/[::]\z/, '').strip
invalid!('Section 名は必須です.') if section_name.blank?
{ 'section_name' => section_name, 'description' => nil }
else
{ 'tag_id' => resolve_tag(label) }
end
node =
if (group_name = TagGroupRepr.name_from_title(label))
group_name = group_name.strip
child_group = TagGroup.find_by(name: group_name)
invalid!("Tag Group #{ group_name.inspect } を解決できません.") unless child_group
invalid!('自分自身を含めることはできません.') if child_group.id == @group&.id
{ 'tag_group_id' => child_group.id }
elsif label.end_with?(':', ':')
section_name = label.sub(/[::]\z/, '').strip
invalid!('Section 名は必須です.') if section_name.blank?
{ 'section_name' => section_name, 'description' => nil }
else
{ 'tag_id' => resolve_tag(locale, label) }
end
node['children'] = nested.flat_map do |list|
invalid!('未対応の入れ子リストです.') unless list.type == :ul
list.children.map { parse_list_item(_1) }
list.children.map { parse_list_item(locale, _1) }
end
node
end
def resolve_tag(name)
names = TagName.where(language_code: 'ja', name:).to_a
def resolve_tag locale, name
names = TagName.where(language_code: locale.language_code, name:).to_a
tag_ids = names.filter_map(&:tag_id).uniq
invalid!("Tag #{ name.inspect } を解決できません.") if tag_ids.empty?
invalid!("Tag 名 #{ name.inspect } が曖昧です.") if tag_ids.many?
+14 -9
ファイルの表示
@@ -19,15 +19,20 @@ class TagGroupMarkdownRenderer
blocks << clean_text(@group['description'])
end
sections = placements.select { _1.key?('section_name') }
if sections.any? && sections.size != placements.size
raise UnsupportedStructure,
'最上位の見出し Section と Placement の混在は Markdown にできません.'
end
first_section = placements.index { _1.key?('section_name') }
if placements.all? { _1.key?('section_name') }
blocks.concat(placements.map { render_heading(_1, 2) })
else
if first_section
if placements.drop(first_section).any? { !_1.key?('section_name') }
raise UnsupportedStructure,
'見出し Section の後に最上位 Placement は配置できません.'
end
if first_section.positive?
blocks << render_list(placements.take(first_section), 0)
end
blocks.concat(placements.drop(first_section).map { render_heading(_1, 2) })
elsif placements.any?
blocks << render_list(placements, 0)
end
@@ -74,7 +79,7 @@ class TagGroupMarkdownRenderer
first_section = section_indexes.first
if children[first_section..].any? { !_1.key?('section_name') }
raise UnsupportedStructure,
'見出し Section と兄弟 Placement の交互配置は Markdown にできません.'
'見出し Section と兄弟 Placement の交互配置は Markdown にできません.'
end
list_nodes = children[0...first_section]
blocks << render_list(list_nodes, 0) if list_nodes.any?
+29 -77
ファイルの表示
@@ -1,49 +1,51 @@
class TagGroupOperation
MAX_PLACEMENT_DEPTH = 5
PREFIX = TagGroupRepr::PREFIX
def self.create!(name:, description:, members:, placements:, actor:,
require_compatibility: false, compatibility_body: nil)
group, sync_issue = ApplicationRecord.transaction do
def self.create!(locale, name:, description:, members:, placements:, actor:,
require_compatibility: false)
ApplicationRecord.transaction do
group = TagGroup.new(version_no: 1, created_by_user: actor,
updated_by_user: actor)
attributes = prepare!(group:, name:, description:, members:, placements:)
attributes = prepare!(locale:, group:, name:, description:, members:, placements:)
group.assign_attributes(name: attributes.fetch(:name),
description: attributes.fetch(:description))
group.save!
persist_structure!(group:, attributes:, actor:)
validate_compatibility!(locale, group) if require_compatibility
persist_version!(group:, attributes:, actor:, event_type: 'create')
issue = sync_compatibility!(group:, actor:, old_name: nil,
require_compatibility:, compatibility_body:)
[group, issue]
group
end
report_sync_issue(group, actor, sync_issue)
group
end
def self.update!(group:, name:, description:, members:, placements:, actor:,
require_compatibility: false, compatibility_body: nil)
group, sync_issue = ApplicationRecord.transaction do
def self.update!(locale, group:, name:, description:, members:, placements:, actor:,
require_compatibility: false)
ApplicationRecord.transaction do
group.lock!
old_name = group.name
attributes = prepare!(group:, name:, description:, members:, placements:)
next [group, nil] if unchanged?(group, attributes)
attributes = prepare!(locale:, group:, name:, description:, members:, placements:)
if unchanged?(group, attributes)
validate_compatibility!(locale, group) if require_compatibility
next group
end
group.update!(name: attributes.fetch(:name),
description: attributes.fetch(:description),
version_no: group.version_no + 1,
updated_by_user: actor)
persist_structure!(group:, attributes:, actor:)
validate_compatibility!(locale, group) if require_compatibility
persist_version!(group:, attributes:, actor:, event_type: 'update')
issue = sync_compatibility!(group:, actor:, old_name:,
require_compatibility:, compatibility_body:)
[group, issue]
group
end
report_sync_issue(group, actor, sync_issue)
group
end
def self.prepare!(group:, name:, description:, members:, placements:)
def self.validate_compatibility! locale, group
TagGroupMarkdownRenderer.render(TagGroupRepr.one(locale, group))
rescue TagGroupMarkdownRenderer::UnsupportedStructure => error
invalid!(group, :base, error.message)
end
private_class_method :validate_compatibility!
def self.prepare!(locale:, group:, name:, description:, members:, placements:)
record = group
normalized_name = name.to_s.strip
invalid!(record, :name, 'グループ名は必須です.') if normalized_name.blank?
@@ -61,7 +63,7 @@ class TagGroupOperation
member_rows = normalize_members(record, members)
placement_tree = normalize_placements(record, placements)
validate_references!(record, member_rows, placement_tree)
validate_references!(locale, record, member_rows, placement_tree)
validate_containment!(record, member_rows)
{ name: normalized_name, description:, members: member_rows,
placements: placement_tree }
@@ -185,7 +187,7 @@ class TagGroupOperation
end
private_class_method :normalize_placement_nodes
def self.validate_references!(record, members, placements)
def self.validate_references!(locale, record, members, placements)
membership = members.to_set
placed = placement_references(placements).uniq
missing = placed - membership.to_a
@@ -204,13 +206,14 @@ class TagGroupOperation
tag_versions = TagVersion.where(tag_id: tag_ids).pluck(:tag_id, :version_no).to_set
group_versions = TagGroupVersion.where(tag_group_id: group_ids)
.pluck(:tag_group_id, :version_no).to_set
tag_names = TagName.where(tag_id: tag_ids, language_code: 'ja', primary_flg: true)
tag_names = TagName.where(
tag_id: tag_ids, language_code: locale.language_code, primary_flg: true)
.pluck(:tag_id).uniq
members.each do |member|
if member.key?('tag_id')
id = member.fetch('tag_id')
invalid!(record, :members, "Tag ##{ id } に日本語の正規名がありません.") unless
invalid!(record, :members, :invalid) unless
tag_names.include?(id)
version_no = tags.fetch(id).version_no
unless tag_versions.include?([id, version_no])
@@ -313,57 +316,6 @@ class TagGroupOperation
end
private_class_method :persist_version!
def self.sync_compatibility!(group:, actor:, old_name:,
require_compatibility:, compatibility_body:)
representation = TagGroupRepr.one(group)
body = begin
TagGroupMarkdownRenderer.render(representation)
rescue TagGroupMarkdownRenderer::UnsupportedStructure => error
if compatibility_body.present?
compatibility_body
elsif require_compatibility
invalid!(group, :base, error.message)
else
return error.message
end
end
invalid!(group, :base, '本文は必須です.') if body.blank?
title = "#{ PREFIX }#{ group.name }"
old_title = old_name && "#{ PREFIX }#{ old_name }"
page = if old_title
WikiPage.joins(:tag_name).find_by(tag_names: { name: old_title })
else
nil
end
page ||= WikiPage.joins(:tag_name).find_by(tag_names: { name: title })
if page
tag_name = page.tag_name
tag_name.update!(name: title) if tag_name.name != title
Wiki::Commit.legacy_content!(page:, body:, created_user: actor, message: nil,
base_revision_id: nil)
else
tag_name = TagName.find_or_initialize_by(language_code: 'ja', name: title)
tag_name.script_code = 'Jpan'
tag_name.primary_flg = true
tag_name.auto_generated = false
tag_name.save!
Wiki::CompatibilityWriter.create!(tag_name:, body:, created_by_user: actor)
end
nil
end
private_class_method :sync_compatibility!
def self.report_sync_issue(group, actor, reason)
return unless reason
payload = { status: 'unsupported_structure', tag_group_id: group.id,
version_no: group.version_no, user_id: actor&.id, reason: }
Rails.logger.warn("tag_group_wiki_sync_skipped #{ payload.to_json }")
end
private_class_method :report_sync_issue
def self.placement_references(nodes)
nodes.flat_map do |node|
own = if node.key?('tag_id')
-164
ファイルの表示
@@ -1,164 +0,0 @@
require 'digest'
module Wiki
class Commit
class Conflict < StandardError
;
end
def self.create_content! tag_name:, body:, created_by_user:, message: nil
if tag_name.tag && tag_name.language_code == 'ja' && !tag_name.name.include?(':')
return TagDescriptionOperation.create!(
tag: tag_name.tag, body:, created_by_user:, message:)
end
tag_name.errors.add(:base, :invalid)
raise ActiveRecord::RecordInvalid, tag_name
end
def self.create_legacy_content! tag_name:, body:, created_by_user:, message: nil
normalised = normalise_body(body)
page = WikiPage.new(tag_name:,
body: normalised,
created_user: created_by_user,
updated_user: created_by_user)
if normalised.blank?
page.errors.add(:body, :blank)
raise ActiveRecord::RecordInvalid, page
end
ActiveRecord::Base.transaction do
page.save!
new(page:, created_user: created_by_user).content!(
body: normalised,
message:,
base_revision_id: nil)
page
end
end
def self.content! page:, body:, created_user:, message: nil, base_revision_id: nil
description = TagDescription.find_by(tag_id: page.tag_name.tag_id, language_code: 'ja')
description ||= TagDescription.joins(:tag).find_by(
tags: { tag_name_id: page.tag_name_id }, language_code: 'ja')
if description && !page.title.include?(':')
return TagDescriptionOperation.update!(
tag: description.tag, body:, updated_by_user: created_user,
message:, base_revision_id:)
end
page.errors.add(:base, :invalid)
raise ActiveRecord::RecordInvalid, page
end
def self.legacy_content! page:, body:, created_user:, message: nil,
base_revision_id: nil
new(page:, created_user:).content!(body:, message:, base_revision_id:)
end
def self.redirect! page:, redirect_page:, created_user:, message: nil, base_revision_id: nil
new(page:, created_user:).redirect!(redirect_page:, message:, base_revision_id:)
end
def initialize page:, created_user:
@page = page
@created_user = created_user
end
def content! body:, message:, base_revision_id:
normalised = self.class.normalise_body(body)
if normalised.blank?
@page.errors.add(:body, :blank)
raise ActiveRecord::RecordInvalid, @page
end
lines = split_lines(normalised)
line_shas = lines.map { |line| Digest::SHA256.hexdigest(line) }
tree_sha = Digest::SHA256.hexdigest(line_shas.join(','))
ActiveRecord::Base.transaction do
@page.lock!
if base_revision_id.present?
current_id = @page.wiki_revisions.maximum(:id)
if current_id && current_id != base_revision_id.to_i
raise Conflict,
"競合が発生してゐます" +
"(現在の Id.:#{ current_id },ベース Id.:#{ base_revision_id })."
end
end
line_id_by_sha = upsert_lines!(lines, line_shas)
line_ids = line_shas.map { |sha| line_id_by_sha.fetch(sha) }
@page.update!(body: normalised)
WikiVersionRecorder.record!(
page: @page,
event_type: @page.wiki_versions.exists? ? :update : :create,
reason: message,
created_by_user: @created_user)
rev = WikiRevision.create!(
wiki_page: @page,
base_revision_id:,
created_user: @created_user,
kind: :content,
redirect_page_id: nil,
message:,
lines_count: lines.length,
tree_sha256: tree_sha)
rows = line_ids.each_with_index.map do |line_id, pos|
{ wiki_revision_id: rev.id, wiki_line_id: line_id, position: pos }
end
WikiRevisionLine.insert_all!(rows) if rows.any?
rev
end
end
def redirect!(redirect_page:, message:, base_revision_id:) = raise '廃止しました.'
def self.normalise_body body
s = body.to_s
s.gsub!(/\r\n?/, "\n")
s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '🖕')
s.gsub(/\n+$/, '')
end
private
def split_lines(body) = body.split("\n")
def upsert_lines! lines, line_shas
now = Time.current
id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
missing_by_sha = { }
line_shas.each_with_index do |sha, i|
next if id_by_sha.key?(sha)
next if missing_by_sha.key?(sha)
missing_by_sha[sha] = {
sha256: sha,
body: lines[i],
created_at: now,
updated_at: now }
end
if missing_by_sha.any?
WikiLine.upsert_all(missing_by_sha.values)
id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
end
id_by_sha
end
end
end
-43
ファイルの表示
@@ -1,43 +0,0 @@
module Wiki
class CompatibilityWriter
def self.create! tag_name:, body:, created_by_user:, message: nil
ApplicationRecord.transaction do
tag_name.lock!
page = WikiPage.lock.find_by(tag_name_id: tag_name.id)
if page
update!(page:, body:, updated_by_user: created_by_user, message:)
page
else
Commit.create_legacy_content!(tag_name:, body:, created_by_user:, message:)
end
end
end
def self.update! page:, body:, updated_by_user:, message: nil,
base_revision_id: nil
normalised = Commit.normalise_body(body)
if normalised.blank?
return Commit.legacy_content!(
page:, body: normalised, created_user: updated_by_user, message:,
base_revision_id:)
end
if base_revision_id.present?
current_id = page.wiki_revisions.maximum(:id)
if current_id && current_id != base_revision_id.to_i
raise Commit::Conflict,
"競合が発生してゐます" +
"(現在の Id.:#{ current_id },ベース Id.:#{ base_revision_id })."
end
end
if Commit.normalise_body(page.body) == normalised && page.wiki_revisions.exists?
return
end
Commit.legacy_content!(
page:, body: normalised, created_user: updated_by_user, message:,
base_revision_id:)
end
end
end
-21
ファイルの表示
@@ -1,21 +0,0 @@
class WikiVersionRecorder < VersionRecorder
def self.record! page:, event_type:, reason: nil, created_by_user:
new(page:, event_type:, reason:, created_by_user:).record!
end
def initialize page:, event_type:, reason: nil, created_by_user:
@reason = reason
super(record: page, event_type:, created_by_user:)
end
private
def version_class = WikiVersion
def version_association = :wiki_versions
def record_key = :wiki_page
def snapshot_attributes = {
title: @record.title,
body: @record.body,
reason: @reason }
end