require 'digest' module Wiki class Commit class Conflict < StandardError ; 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 = normalise_body(body) 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 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) rev end end def redirect! redirect_page:, message:, base_revision_id: 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 WikiRevision.create!( wiki_page: @page, base_revision_id:, created_user: @created_user, kind: :redirect, redirect_page:, message:, lines_count: 0, tree_sha256: nil) end end private def normalise_body body s = body.to_s s.gsub!("\r\n", "\n") s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '🖕') end def split_lines body body.split("\n") end 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