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 => ( -