require 'digest' module Wiki class Commit class Conflict < StandardError ; end def self.create_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 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(',')) line_id_by_sha = upsert_lines!(lines, line_shas) line_ids = line_shas.map { |sha| line_id_by_sha.fetch(sha) } 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 @page.update!(body: normalised) WikiVersionRecorder.record!( page: @page, event_type: @page.wiki_versions.exists? ? :update : :create, reason: message, created_by_user: @created_user) tag = @page.tag_name.tag if tag&.tag_versions&.exists? TagVersionRecorder.record!(tag:, event_type: :update, created_by_user: @created_user) end 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_rows = [] line_shas.each_with_index do |sha, i| next if id_by_sha.key?(sha) missing_rows << { sha256: sha, body: lines[i], created_at: now, updated_at: now } end if missing_rows.any? WikiLine.upsert_all(missing_rows) id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h end id_by_sha end end end