diff --git a/backend/app/controllers/wiki_pages_controller.rb b/backend/app/controllers/wiki_pages_controller.rb index abe29f0..c4dedf0 100644 --- a/backend/app/controllers/wiki_pages_controller.rb +++ b/backend/app/controllers/wiki_pages_controller.rb @@ -1,8 +1,8 @@ class WikiPagesController < ApplicationController - def index - wiki_pages = WikiPage.all + rescue_from Wiki::Commit::Conflict, with: :render_wiki_conflict - render json: wiki_pages + def index + render json: WikiPage.all end def show @@ -31,21 +31,25 @@ class WikiPagesController < ApplicationController def diff id = params[:id] - from = params[:from] + return head :bad_request if id.blank? + + from = params[:from].presence to = params[:to].presence - return head :bad_request if id.blank? || from.blank? - wiki_page_from = WikiPage.find(id) - wiki_page_to = WikiPage.find(id) - wiki_page_from.sha = from - wiki_page_to.sha = to + page = WikiPage.find(id) + + from_rev = from && page.wiki_revisions.find(from) + to_rev = to ? page.wiki_revisions.find(to) : page.current_revision + if ((from_rev && !(from_rev.content?)) || !(to_rev&.content?)) + return head :unprocessable_entity + end - diffs = Diff::LCS.sdiff(wiki_page_from.body, wiki_page_to.body) + diffs = Diff::LCS.sdiff(from_rev&.body&.lines || [], to_rev.body.lines) diff_json = diffs.map { |change| case change.action when ?= { type: 'context', content: change.old_element } - when ?| + when ?! [{ type: 'removed', content: change.old_element }, { type: 'added', content: change.new_element }] when ?+ @@ -55,23 +59,32 @@ class WikiPagesController < ApplicationController end }.flatten.compact - render json: { wiki_page_id: wiki_page_from.id, - title: wiki_page_from.title, - older_sha: wiki_page_from.sha, - newer_sha: wiki_page_to.sha, - diff: diff_json } + render json: { wiki_page_id: page.id, + title: page.title, + older_revision_id: from_rev&.id, + newer_revision_id: to_rev.id, + diff: diff_json } end def create return head :unauthorized unless current_user - return head :forbidden unless ['admin', 'member'].include?(current_user.role) + return head :forbidden unless current_user.member? + + title = params[:title]&.strip + body = params[:body].to_s - wiki_page = WikiPage.new(title: params[:title], created_user: current_user, updated_user: current_user) - if wiki_page.save - wiki_page.set_body params[:body], user: current_user - render json: wiki_page, status: :created + return head :unprocessable_entity if title.blank? || body.blank? + + page = WikiPage.new(title:, created_user: current_user, updated_user: current_user) + + if page.save + message = params[:message].presence + Wiki::Commit.content!(page:, body:, created_user: current_user, message:) + + render json: page, status: :created else - render json: { errors: wiki_page.errors.full_messages }, status: :unprocessable_entity + render json: { errors: page.errors.full_messages }, + status: :unprocessable_entity end end @@ -79,16 +92,24 @@ class WikiPagesController < ApplicationController return head :unauthorized unless current_user return head :forbidden unless current_user.member? - title = params[:title] - body = params[:body] + title = params[:title]&.strip + body = params[:body].to_s return head :unprocessable_entity if title.blank? || body.blank? - wiki_page = WikiPage.find(params[:id]) - wiki_page.title = title - wiki_page.updated_user = current_user - wiki_page.set_body(body, user: current_user) - wiki_page.save! + page = WikiPage.find(params[:id]) + base_revision_id = page.current_revision.id + + if params[:title].present? && params[:title].strip != page.title + return head :unprocessable_entity + end + + message = params[:message].presence + Wiki::Commit.content!(page:, + body:, + created_user: current_user, + message:, + base_revision_id:) head :ok end @@ -97,55 +118,73 @@ class WikiPagesController < ApplicationController title = params[:title]&.strip q = WikiPage.all - q = q.where('title LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%") if title.present? + if title.present? + q = q.where('title LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%") + end render json: q.limit(20) end def changes - id = params[:id] - log = if id.present? - wiki.page("#{ id }.md")&.versions - else - wiki.repo.log('main', nil) - end - return render json: [] unless log - - render json: log.map { |commit| - wiki_page = WikiPage.find(commit.message.split(' ')[1].to_i) - wiki_page.sha = commit.id - - next nil if wiki_page.sha.blank? - - user = User.find(commit.author.name.to_i) - - { sha: wiki_page.sha, - pred: wiki_page.pred, - succ: wiki_page.succ, - wiki_page: wiki_page && { id: wiki_page.id, title: wiki_page.title }, - user: user && { id: user.id, name: user.name }, - change_type: commit.message.split(' ')[0].downcase[0...(-1)], - timestamp: commit.authored_date } + id = params[:id].presence + q = WikiRevision.includes(:wiki_page, :created_user).order(id: :desc) + q = q.where(wiki_page_id: id) if id + + render json: q.limit(200).map { |rev| + { revision_id: rev.id, + pred: rev.base_revision_id, + succ: nil, + wiki_page: { id: rev.wiki_page_id, title: rev.wiki_page.title }, + user: { id: rev.created_user.id, name: rev.created_user.name }, + kind: rev.kind, + message: rev.message, + timestamp: rev.created_at } }.compact end private - WIKI_PATH = Rails.root.join('wiki').to_s + def render_wiki_page_or_404 page + return head :not_found unless page - def wiki - @wiki ||= Gollum::Wiki.new(WIKI_PATH) - end + if params[:version].present? + rev = page.wiki_revisions.find_by(id: params[:version]) + return head :not_found unless rev + + if rev.redirect? + return ( + redirect_to wiki_page_by_title_path(title: rev.redirect_page.title), + status: :moved_permanently) + end + + body = rev.body + revision_id = rev.id + pred = page.pred_revision_id(revision_id) + succ = page.succ_revision_id(revision_id) - def render_wiki_page_or_404 wiki_page - return head :not_found unless wiki_page + return render json: page.as_json.merge(body:, revision_id:, pred:, succ:) + end + + rev = page.current_revision + unless rev + return render json: page.as_json.merge(body: nil, revision_id: nil, pred: nil, succ: nil) + end - wiki_page.sha = params[:version].presence + if rev.redirect? + return ( + redirect_to wiki_page_by_title_path(title: rev.redirect_page.title), + status: :moved_permanently) + end + + body = rev.body + revision_id = rev.id + pred = page.pred_revision_id(revision_id) + succ = page.succ_revision_id(revision_id) + + render json: page.as_json.merge(body:, revision_id:, pred:, succ:) + end - body = wiki_page.body - sha = wiki_page.sha - pred = wiki_page.pred - succ = wiki_page.succ - render json: wiki_page.as_json.merge(body:, sha:, pred:, succ:) + def render_wiki_conflict err + render json: { error: 'conflict', message: err.message }, status: :conflict end end diff --git a/backend/app/models/wiki_line.rb b/backend/app/models/wiki_line.rb new file mode 100644 index 0000000..c169917 --- /dev/null +++ b/backend/app/models/wiki_line.rb @@ -0,0 +1,15 @@ +class WikiLine < ApplicationRecord + has_many :wiki_revision_lines, dependent: :restrict_with_exception + + validates :sha256, presence: true, uniqueness: true, length: { is: 64 } + validates :body, presence: true + + def self.upsert_by_body! body + sha = Digest::SHA256.hexdigest(body) + now = Time.current + + upsert({ sha256: sha, body:, created_at: now, updated_at: now }) + + find_by!(sha256: sha) + end +end diff --git a/backend/app/models/wiki_page.rb b/backend/app/models/wiki_page.rb index 2e57372..256d4df 100644 --- a/backend/app/models/wiki_page.rb +++ b/backend/app/models/wiki_page.rb @@ -1,80 +1,50 @@ -require 'gollum-lib' +require 'set' class WikiPage < ApplicationRecord - belongs_to :tag, optional: true - belongs_to :created_user, class_name: 'User', foreign_key: 'created_user_id' - belongs_to :updated_user, class_name: 'User', foreign_key: 'updated_user_id' + has_many :wiki_revisions, dependent: :destroy + belongs_to :created_user, class_name: 'User' + belongs_to :updated_user, class_name: 'User' + + has_many :redirected_from_revisions, + class_name: 'WikiRevision', + foreign_key: :redirect_page_id, + dependent: :nullify validates :title, presence: true, length: { maximum: 255 }, uniqueness: true - def as_json options = { } - self.sha = nil - super options + def current_revision + wiki_revisions.order(id: :desc).first end - def sha= val - if val.present? - @sha = val - @page = wiki.page("#{ id }.md", @sha) - else - @page = wiki.page("#{ id }.md") - @sha = @page.versions.first.id - end - vers = @page.versions - idx = vers.find_index { |ver| ver.id == @sha } - if idx - @pred = vers[idx + 1]&.id - @succ = idx.positive? ? vers[idx - 1].id : nil - @updated_at = vers[idx].authored_date - else - @sha = nil - @pred = nil - @succ = nil - @updated_at = nil - end - @sha - end - - def sha - @sha + def body + rev = current_revision + rev.body if rev&.content? end - def pred - @pred - end + def resolve_redirect limit: 10 + page = self + visited = Set.new - def succ - @succ - end + limit.times do + return page if visited.include?(page.id) - def updated_at - @updated_at - end + visited.add(page.id) - def body - sha = nil unless @page - @page&.raw_data&.force_encoding('UTF-8') - end + rev = page.current_revision + return page if !(rev&.redirect?) || !(rev.redirect_page) - def set_body content, user: - commit_info = { name: user.id.to_s, - email: 'dummy@example.com' } - page = wiki.page("#{ id }.md") - if page - commit_info[:message] = "Updated #{ id }" - wiki.update_page(page, id.to_s, :markdown, content, commit_info) - else - commit_info[:message] = "Created #{ id }" - wiki.write_page(id.to_s, :markdown, content, commit_info) + page = rev.redirect_page end - end - private + page + end - WIKI_PATH = Rails.root.join('wiki').to_s + def pred_revision_id revision_id + wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id) + end - def wiki - @wiki ||= Gollum::Wiki.new(WIKI_PATH) + def succ_revision_id revision_id + wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id) end end diff --git a/backend/app/models/wiki_revision.rb b/backend/app/models/wiki_revision.rb new file mode 100644 index 0000000..da6ca7d --- /dev/null +++ b/backend/app/models/wiki_revision.rb @@ -0,0 +1,55 @@ +class WikiRevision < ApplicationRecord + belongs_to :wiki_page + belongs_to :base_revision, class_name: 'WikiRevision', optional: true + belongs_to :created_user, class_name: 'User' + belongs_to :redirect_page, class_name: 'WikiPage', optional: true + + has_many :wiki_revision_lines, dependent: :delete_all + has_many :wiki_lines, through: :wiki_revision_lines + + enum :kind, { content: 0, redirect: 1 } + + validates :kind, presence: true + validates :lines_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :tree_sha256, length: { is: 64 }, allow_nil: true + + validate :kind_consistency + + def body + return unless content? + + wiki_revision_lines + .includes(:wiki_line) + .order(:position) + .map { |rev| rev.wiki_line.body } + .join("\n") + end + + private + + def kind_consistency + if content? + if tree_sha256.blank? + errors.add(:tree_sha256, '種類がページの場合は必須です.') + end + + if redirect_page_id.present? + errors.add(:redirect_page_id, '種類がページの場合は空である必要があります.') + end + end + + if redirect? + if redirect_page_id.blank? + errors.add(:redirect_page_id, '種類がリダイレクトの場合は必須です.') + end + + if tree_sha256.present? + errors.add(:tree_sha256, '種類がリダイレクトの場合は空である必要があります.') + end + + if lines_count.to_i > 0 + errors.add(:lines_count, '種類がリダイレクトの場合は 0 である必要があります.') + end + end + end +end diff --git a/backend/app/models/wiki_revision_line.rb b/backend/app/models/wiki_revision_line.rb new file mode 100644 index 0000000..8d58642 --- /dev/null +++ b/backend/app/models/wiki_revision_line.rb @@ -0,0 +1,8 @@ +class WikiRevisionLine < ApplicationRecord + belongs_to :wiki_revision + belongs_to :wiki_line + + validates :position, presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :position, uniqueness: { scope: :wiki_revision_id } +end diff --git a/backend/app/services/wiki/commit.rb b/backend/app/services/wiki/commit.rb new file mode 100644 index 0000000..c0be98a --- /dev/null +++ b/backend/app/services/wiki/commit.rb @@ -0,0 +1,122 @@ +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 diff --git a/backend/db/migrate/20251229012100_remove_tag_from_wiki_pages.rb b/backend/db/migrate/20251229012100_remove_tag_from_wiki_pages.rb new file mode 100644 index 0000000..f4ef842 --- /dev/null +++ b/backend/db/migrate/20251229012100_remove_tag_from_wiki_pages.rb @@ -0,0 +1,5 @@ +class RemoveTagFromWikiPages < ActiveRecord::Migration[7.0] + def change + remove_reference :wiki_pages, :tag, if_exists: true + end +end diff --git a/backend/db/migrate/20251229020700_create_wiki_lines.rb b/backend/db/migrate/20251229020700_create_wiki_lines.rb new file mode 100644 index 0000000..cba6fb4 --- /dev/null +++ b/backend/db/migrate/20251229020700_create_wiki_lines.rb @@ -0,0 +1,11 @@ +class CreateWikiLines < ActiveRecord::Migration[7.0] + def change + create_table :wiki_lines do |t| + t.string :sha256, null: false, limit: 64 + t.text :body, null: false + t.timestamps + end + + add_index :wiki_lines, :sha256, unique: true + end +end diff --git a/backend/db/migrate/20251229021000_create_wiki_revisions.rb b/backend/db/migrate/20251229021000_create_wiki_revisions.rb new file mode 100644 index 0000000..5b80621 --- /dev/null +++ b/backend/db/migrate/20251229021000_create_wiki_revisions.rb @@ -0,0 +1,19 @@ +class CreateWikiRevisions < ActiveRecord::Migration[7.0] + def change + create_table :wiki_revisions do |t| + t.references :wiki_page, null: false, foreign_key: true + t.references :base_revision, foreign_key: { to_table: :wiki_revisions } + t.references :created_user, null: false, foreign_key: { to_table: :users } + t.integer :kind, null: false, default: 0 # 0: content, 1: redirect + t.references :redirect_page, foreign_key: { to_table: :wiki_pages } + t.string :message + t.integer :lines_count, null: false, default: 0 + t.string :tree_sha256, limit: 64 + t.timestamps + end + + add_index :wiki_revisions, :tree_sha256 + add_index :wiki_revisions, [:wiki_page_id, :id] + add_index :wiki_revisions, :kind + end +end diff --git a/backend/db/migrate/20251229022100_create_wiki_revision_lines.rb b/backend/db/migrate/20251229022100_create_wiki_revision_lines.rb new file mode 100644 index 0000000..e62a7c4 --- /dev/null +++ b/backend/db/migrate/20251229022100_create_wiki_revision_lines.rb @@ -0,0 +1,12 @@ +class CreateWikiRevisionLines < ActiveRecord::Migration[7.0] + def change + create_table :wiki_revision_lines do |t| + t.references :wiki_revision, null: false, foreign_key: true + t.integer :position, null: false + t.references :wiki_line, null: false, foreign_key: true + end + + add_index :wiki_revision_lines, [:wiki_revision_id, :position], unique: true + add_index :wiki_revision_lines, [:wiki_revision_id, :wiki_line_id] + end +end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index 6a26dfd..116a1a2 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_12_10_123200) do +ActiveRecord::Schema[8.0].define(version: 2025_12_29_022100) do create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -167,18 +167,54 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_10_123200) do t.datetime "updated_at", null: false end + create_table "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "sha256", limit: 64, null: false + t.text "body", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["sha256"], name: "index_wiki_lines_on_sha256", unique: true + end + create_table "wiki_pages", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "title", null: false - t.bigint "tag_id" t.bigint "created_user_id", null: false t.bigint "updated_user_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id" - t.index ["tag_id"], name: "index_wiki_pages_on_tag_id" t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id" end + create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.bigint "wiki_revision_id", null: false + t.integer "position", null: false + t.bigint "wiki_line_id", null: false + t.index ["wiki_line_id"], name: "index_wiki_revision_lines_on_wiki_line_id" + t.index ["wiki_revision_id", "position"], name: "index_wiki_revision_lines_on_wiki_revision_id_and_position", unique: true + t.index ["wiki_revision_id", "wiki_line_id"], name: "index_wiki_revision_lines_on_wiki_revision_id_and_wiki_line_id" + t.index ["wiki_revision_id"], name: "index_wiki_revision_lines_on_wiki_revision_id" + end + + create_table "wiki_revisions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.bigint "wiki_page_id", null: false + t.bigint "base_revision_id" + t.bigint "created_user_id", null: false + t.integer "kind", default: 0, null: false + t.bigint "redirect_page_id" + t.string "message" + t.integer "lines_count", default: 0, null: false + t.string "tree_sha256", limit: 64 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["base_revision_id"], name: "index_wiki_revisions_on_base_revision_id" + t.index ["created_user_id"], name: "index_wiki_revisions_on_created_user_id" + t.index ["kind"], name: "index_wiki_revisions_on_kind" + t.index ["redirect_page_id"], name: "index_wiki_revisions_on_redirect_page_id" + t.index ["tree_sha256"], name: "index_wiki_revisions_on_tree_sha256" + t.index ["wiki_page_id", "id"], name: "index_wiki_revisions_on_wiki_page_id_and_id" + t.index ["wiki_page_id"], name: "index_wiki_revisions_on_wiki_page_id" + end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "nico_tag_relations", "tags" @@ -201,7 +237,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_10_123200) do add_foreign_key "user_ips", "users" add_foreign_key "user_post_views", "posts" add_foreign_key "user_post_views", "users" - add_foreign_key "wiki_pages", "tags" add_foreign_key "wiki_pages", "users", column: "created_user_id" add_foreign_key "wiki_pages", "users", column: "updated_user_id" + add_foreign_key "wiki_revision_lines", "wiki_lines" + add_foreign_key "wiki_revision_lines", "wiki_revisions" + add_foreign_key "wiki_revisions", "users", column: "created_user_id" + add_foreign_key "wiki_revisions", "wiki_pages" + add_foreign_key "wiki_revisions", "wiki_pages", column: "redirect_page_id" + add_foreign_key "wiki_revisions", "wiki_revisions", column: "base_revision_id" end diff --git a/backend/lib/tasks/migrate_wiki.rake b/backend/lib/tasks/migrate_wiki.rake new file mode 100644 index 0000000..1fd7ad1 --- /dev/null +++ b/backend/lib/tasks/migrate_wiki.rake @@ -0,0 +1,73 @@ +namespace :wiki do + desc 'Wiki 移行' + task migrate: :environment do + require 'digest' + require 'gollum-lib' + + wiki = Gollum::Wiki.new(Rails.root.join('wiki').to_s) + + WikiPage.where.missing(:wiki_revisions).find_each do |wiki_page| + page = wiki.page("#{ wiki_page.id }.md") + next unless page + + versions = page.versions + next if versions.blank? + + base_revision_id = nil + versions.reverse_each do |version| + pg = wiki.page("#{ wiki_page.id }.md", version.id) + raw = pg&.raw_data + next unless raw + + lines = raw.force_encoding('UTF-8').split("\n") + + line_shas = lines.map { |l| Digest::SHA256.hexdigest(l) } + tree_sha = Digest::SHA256.hexdigest(line_shas.join(',')) + + at = version.authored_date + + line_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 line_id_by_sha.key?(sha) + + missing_rows << { sha256: sha, + body: lines[i], + created_at: at, + updated_at: at } + end + + if missing_rows.any? + WikiLine.upsert_all(missing_rows) + line_id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h + end + line_ids = line_shas.map { |sha| line_id_by_sha.fetch(sha) } + + ActiveRecord::Base.transaction do + wiki_page.lock! + + rev = WikiRevision.create!( + wiki_page:, + base_revision_id:, + created_user_id: Integer(version.author.name) rescue 2, + kind: :content, + redirect_page_id: nil, + message: nil, + lines_count: lines.length, + tree_sha256: tree_sha, + created_at: at, + updated_at: at) + + 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) + end + base_revision_id = rev.id + end + end + end +end diff --git a/frontend/src/pages/wiki/WikiDiffPage.tsx b/frontend/src/pages/wiki/WikiDiffPage.tsx index 85a62f0..e99033b 100644 --- a/frontend/src/pages/wiki/WikiDiffPage.tsx +++ b/frontend/src/pages/wiki/WikiDiffPage.tsx @@ -40,10 +40,10 @@ export default () => { {diff ? ( diff.diff.map (d => ( - - {d.content == '\n' ?
: d.content} -
))) +

+ {d.content} +

))) : 'Loading...'} ) diff --git a/frontend/src/pages/wiki/WikiHistoryPage.tsx b/frontend/src/pages/wiki/WikiHistoryPage.tsx index ba1d71b..000ce43 100644 --- a/frontend/src/pages/wiki/WikiHistoryPage.tsx +++ b/frontend/src/pages/wiki/WikiHistoryPage.tsx @@ -41,30 +41,20 @@ export default () => { {changes.map (change => ( - + - {change.changeType === 'update' && ( - + {change.pred != null && ( + 差分 )} - + {change.wikiPage.title} - {(() => { - switch (change.changeType) - { - case 'create': - return '新規' - case 'update': - return '更新' - case 'delete': - return '削除' - } - }) ()} + {change.pred == null ? '新規' : '更新'} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index f78c85b..0dccb59 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -59,29 +59,33 @@ export type User = { export type ViewFlagBehavior = typeof ViewFlagBehavior[keyof typeof ViewFlagBehavior] export type WikiPage = { - id: number - title: string - body: string - sha: string - pred?: string - succ?: string - updatedAt?: string } + id: number + title: string + createdUserId: number + updatedUserId: number + createdAt: string + updatedAt: string + body: string + revisionId: number + pred: number | null + succ: number | null } export type WikiPageChange = { - sha: string - pred?: string - succ?: string - wikiPage: WikiPage - user: User - changeType: string + revisionId: number + pred: number | null + succ: null + wikiPage: Pick + user: Pick + kind: 'content' | 'redirect' + message: string | null timestamp: string } export type WikiPageDiff = { - wikiPageId: number - title: string - olderSha: string - newerSha: string - diff: WikiPageDiffDiff[] } + wikiPageId: number + title: string + olderRevisionId: number | null + newerRevisionId: number + diff: WikiPageDiffDiff[] } export type WikiPageDiffDiff = { type: 'context' | 'added' | 'removed'