feat: Wiki の管理方法変更(#188) (#195)
Merge branch 'feature/188' of https://git.miteruzo.com/miteruzo/btrc-hub into feature/188 #188 Merge branch 'main' into feature/188 #188 #188 #188 #188 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #195
This commit was merged in pull request #195.
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
class WikiPagesController < ApplicationController
|
class WikiPagesController < ApplicationController
|
||||||
def index
|
rescue_from Wiki::Commit::Conflict, with: :render_wiki_conflict
|
||||||
wiki_pages = WikiPage.all
|
|
||||||
|
|
||||||
render json: wiki_pages
|
def index
|
||||||
|
render json: WikiPage.all
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -31,21 +31,25 @@ class WikiPagesController < ApplicationController
|
|||||||
|
|
||||||
def diff
|
def diff
|
||||||
id = params[:id]
|
id = params[:id]
|
||||||
from = params[:from]
|
return head :bad_request if id.blank?
|
||||||
|
|
||||||
|
from = params[:from].presence
|
||||||
to = params[:to].presence
|
to = params[:to].presence
|
||||||
return head :bad_request if id.blank? || from.blank?
|
|
||||||
|
|
||||||
wiki_page_from = WikiPage.find(id)
|
page = WikiPage.find(id)
|
||||||
wiki_page_to = WikiPage.find(id)
|
|
||||||
wiki_page_from.sha = from
|
|
||||||
wiki_page_to.sha = to
|
|
||||||
|
|
||||||
diffs = Diff::LCS.sdiff(wiki_page_from.body, wiki_page_to.body)
|
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(from_rev&.body&.lines || [], to_rev.body.lines)
|
||||||
diff_json = diffs.map { |change|
|
diff_json = diffs.map { |change|
|
||||||
case change.action
|
case change.action
|
||||||
when ?=
|
when ?=
|
||||||
{ type: 'context', content: change.old_element }
|
{ type: 'context', content: change.old_element }
|
||||||
when ?|
|
when ?!
|
||||||
[{ type: 'removed', content: change.old_element },
|
[{ type: 'removed', content: change.old_element },
|
||||||
{ type: 'added', content: change.new_element }]
|
{ type: 'added', content: change.new_element }]
|
||||||
when ?+
|
when ?+
|
||||||
@@ -55,23 +59,32 @@ class WikiPagesController < ApplicationController
|
|||||||
end
|
end
|
||||||
}.flatten.compact
|
}.flatten.compact
|
||||||
|
|
||||||
render json: { wiki_page_id: wiki_page_from.id,
|
render json: { wiki_page_id: page.id,
|
||||||
title: wiki_page_from.title,
|
title: page.title,
|
||||||
older_sha: wiki_page_from.sha,
|
older_revision_id: from_rev&.id,
|
||||||
newer_sha: wiki_page_to.sha,
|
newer_revision_id: to_rev.id,
|
||||||
diff: diff_json }
|
diff: diff_json }
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless ['admin', 'member'].include?(current_user.role)
|
return head :forbidden unless current_user.member?
|
||||||
|
|
||||||
wiki_page = WikiPage.new(title: params[:title], created_user: current_user, updated_user: current_user)
|
title = params[:title]&.strip
|
||||||
if wiki_page.save
|
body = params[:body].to_s
|
||||||
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
|
else
|
||||||
render json: { errors: wiki_page.errors.full_messages }, status: :unprocessable_entity
|
render json: { errors: page.errors.full_messages },
|
||||||
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -79,16 +92,24 @@ class WikiPagesController < ApplicationController
|
|||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.member?
|
return head :forbidden unless current_user.member?
|
||||||
|
|
||||||
title = params[:title]
|
title = params[:title]&.strip
|
||||||
body = params[:body]
|
body = params[:body].to_s
|
||||||
|
|
||||||
return head :unprocessable_entity if title.blank? || body.blank?
|
return head :unprocessable_entity if title.blank? || body.blank?
|
||||||
|
|
||||||
wiki_page = WikiPage.find(params[:id])
|
page = WikiPage.find(params[:id])
|
||||||
wiki_page.title = title
|
base_revision_id = page.current_revision.id
|
||||||
wiki_page.updated_user = current_user
|
|
||||||
wiki_page.set_body(body, user: current_user)
|
if params[:title].present? && params[:title].strip != page.title
|
||||||
wiki_page.save!
|
return head :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
message = params[:message].presence
|
||||||
|
Wiki::Commit.content!(page:,
|
||||||
|
body:,
|
||||||
|
created_user: current_user,
|
||||||
|
message:,
|
||||||
|
base_revision_id:)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
@@ -97,55 +118,73 @@ class WikiPagesController < ApplicationController
|
|||||||
title = params[:title]&.strip
|
title = params[:title]&.strip
|
||||||
|
|
||||||
q = WikiPage.all
|
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)
|
render json: q.limit(20)
|
||||||
end
|
end
|
||||||
|
|
||||||
def changes
|
def changes
|
||||||
id = params[:id]
|
id = params[:id].presence
|
||||||
log = if id.present?
|
q = WikiRevision.includes(:wiki_page, :created_user).order(id: :desc)
|
||||||
wiki.page("#{ id }.md")&.versions
|
q = q.where(wiki_page_id: id) if id
|
||||||
else
|
|
||||||
wiki.repo.log('main', nil)
|
|
||||||
end
|
|
||||||
return render json: [] unless log
|
|
||||||
|
|
||||||
render json: log.map { |commit|
|
render json: q.limit(200).map { |rev|
|
||||||
wiki_page = WikiPage.find(commit.message.split(' ')[1].to_i)
|
{ revision_id: rev.id,
|
||||||
wiki_page.sha = commit.id
|
pred: rev.base_revision_id,
|
||||||
|
succ: nil,
|
||||||
next nil if wiki_page.sha.blank?
|
wiki_page: { id: rev.wiki_page_id, title: rev.wiki_page.title },
|
||||||
|
user: { id: rev.created_user.id, name: rev.created_user.name },
|
||||||
user = User.find(commit.author.name.to_i)
|
kind: rev.kind,
|
||||||
|
message: rev.message,
|
||||||
{ sha: wiki_page.sha,
|
timestamp: rev.created_at }
|
||||||
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 }
|
|
||||||
}.compact
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
WIKI_PATH = Rails.root.join('wiki').to_s
|
def render_wiki_page_or_404 page
|
||||||
|
return head :not_found unless page
|
||||||
|
|
||||||
def wiki
|
if params[:version].present?
|
||||||
@wiki ||= Gollum::Wiki.new(WIKI_PATH)
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
def render_wiki_page_or_404 wiki_page
|
def render_wiki_conflict err
|
||||||
return head :not_found unless wiki_page
|
render json: { error: 'conflict', message: err.message }, status: :conflict
|
||||||
|
|
||||||
wiki_page.sha = params[:version].presence
|
|
||||||
|
|
||||||
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:)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,80 +1,50 @@
|
|||||||
require 'gollum-lib'
|
require 'set'
|
||||||
|
|
||||||
|
|
||||||
class WikiPage < ApplicationRecord
|
class WikiPage < ApplicationRecord
|
||||||
belongs_to :tag, optional: true
|
has_many :wiki_revisions, dependent: :destroy
|
||||||
belongs_to :created_user, class_name: 'User', foreign_key: 'created_user_id'
|
belongs_to :created_user, class_name: 'User'
|
||||||
belongs_to :updated_user, class_name: 'User', foreign_key: 'updated_user_id'
|
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
|
validates :title, presence: true, length: { maximum: 255 }, uniqueness: true
|
||||||
|
|
||||||
def as_json options = { }
|
def current_revision
|
||||||
self.sha = nil
|
wiki_revisions.order(id: :desc).first
|
||||||
super options
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
def pred
|
|
||||||
@pred
|
|
||||||
end
|
|
||||||
|
|
||||||
def succ
|
|
||||||
@succ
|
|
||||||
end
|
|
||||||
|
|
||||||
def updated_at
|
|
||||||
@updated_at
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def body
|
def body
|
||||||
sha = nil unless @page
|
rev = current_revision
|
||||||
@page&.raw_data&.force_encoding('UTF-8')
|
rev.body if rev&.content?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body content, user:
|
def resolve_redirect limit: 10
|
||||||
commit_info = { name: user.id.to_s,
|
page = self
|
||||||
email: 'dummy@example.com' }
|
visited = Set.new
|
||||||
page = wiki.page("#{ id }.md")
|
|
||||||
if page
|
limit.times do
|
||||||
commit_info[:message] = "Updated #{ id }"
|
return page if visited.include?(page.id)
|
||||||
wiki.update_page(page, id.to_s, :markdown, content, commit_info)
|
|
||||||
else
|
visited.add(page.id)
|
||||||
commit_info[:message] = "Created #{ id }"
|
|
||||||
wiki.write_page(id.to_s, :markdown, content, commit_info)
|
rev = page.current_revision
|
||||||
|
return page if !(rev&.redirect?) || !(rev.redirect_page)
|
||||||
|
|
||||||
|
page = rev.redirect_page
|
||||||
end
|
end
|
||||||
|
|
||||||
|
page
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def pred_revision_id revision_id
|
||||||
|
wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id)
|
||||||
|
end
|
||||||
|
|
||||||
WIKI_PATH = Rails.root.join('wiki').to_s
|
def succ_revision_id revision_id
|
||||||
|
wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id)
|
||||||
def wiki
|
|
||||||
@wiki ||= Gollum::Wiki.new(WIKI_PATH)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class RemoveTagFromWikiPages < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
remove_reference :wiki_pages, :tag, if_exists: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Generated
+45
-4
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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|
|
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", 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
|
t.datetime "updated_at", null: false
|
||||||
end
|
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|
|
create_table "wiki_pages", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "title", null: false
|
t.string "title", null: false
|
||||||
t.bigint "tag_id"
|
|
||||||
t.bigint "created_user_id", null: false
|
t.bigint "created_user_id", null: false
|
||||||
t.bigint "updated_user_id", null: false
|
t.bigint "updated_user_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
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"
|
t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id"
|
||||||
end
|
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_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "active_storage_variant_records", "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"
|
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_ips", "users"
|
||||||
add_foreign_key "user_post_views", "posts"
|
add_foreign_key "user_post_views", "posts"
|
||||||
add_foreign_key "user_post_views", "users"
|
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: "created_user_id"
|
||||||
add_foreign_key "wiki_pages", "users", column: "updated_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
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -40,10 +40,10 @@ export default () => {
|
|||||||
{diff
|
{diff
|
||||||
? (
|
? (
|
||||||
diff.diff.map (d => (
|
diff.diff.map (d => (
|
||||||
<span className={cn (d.type === 'added' && 'bg-green-200 dark:bg-green-800',
|
<p className={cn (d.type === 'added' && 'bg-green-200 dark:bg-green-800',
|
||||||
d.type === 'removed' && 'bg-red-200 dark:bg-red-800')}>
|
d.type === 'removed' && 'bg-red-200 dark:bg-red-800')}>
|
||||||
{d.content == '\n' ? <br/> : d.content}
|
{d.content}
|
||||||
</span>)))
|
</p>)))
|
||||||
: 'Loading...'}
|
: 'Loading...'}
|
||||||
</div>
|
</div>
|
||||||
</MainArea>)
|
</MainArea>)
|
||||||
|
|||||||
@@ -41,30 +41,20 @@ export default () => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{changes.map (change => (
|
{changes.map (change => (
|
||||||
<tr key={change.sha}>
|
<tr key={change.revisionId}>
|
||||||
<td>
|
<td>
|
||||||
{change.changeType === 'update' && (
|
{change.pred != null && (
|
||||||
<Link to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.sha }`}>
|
<Link to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.revisionId }`}>
|
||||||
差分
|
差分
|
||||||
</Link>)}
|
</Link>)}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<Link to={`/wiki/${ encodeURIComponent (change.wikiPage.title) }?version=${ change.sha }`}>
|
<Link to={`/wiki/${ encodeURIComponent (change.wikiPage.title) }?version=${ change.revisionId }`}>
|
||||||
{change.wikiPage.title}
|
{change.wikiPage.title}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
{(() => {
|
{change.pred == null ? '新規' : '更新'}
|
||||||
switch (change.changeType)
|
|
||||||
{
|
|
||||||
case 'create':
|
|
||||||
return '新規'
|
|
||||||
case 'update':
|
|
||||||
return '更新'
|
|
||||||
case 'delete':
|
|
||||||
return '削除'
|
|
||||||
}
|
|
||||||
}) ()}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<Link to={`/users/${ change.user.id }`}>
|
<Link to={`/users/${ change.user.id }`}>
|
||||||
|
|||||||
+22
-18
@@ -59,29 +59,33 @@ export type User = {
|
|||||||
export type ViewFlagBehavior = typeof ViewFlagBehavior[keyof typeof ViewFlagBehavior]
|
export type ViewFlagBehavior = typeof ViewFlagBehavior[keyof typeof ViewFlagBehavior]
|
||||||
|
|
||||||
export type WikiPage = {
|
export type WikiPage = {
|
||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
body: string
|
createdUserId: number
|
||||||
sha: string
|
updatedUserId: number
|
||||||
pred?: string
|
createdAt: string
|
||||||
succ?: string
|
updatedAt: string
|
||||||
updatedAt?: string }
|
body: string
|
||||||
|
revisionId: number
|
||||||
|
pred: number | null
|
||||||
|
succ: number | null }
|
||||||
|
|
||||||
export type WikiPageChange = {
|
export type WikiPageChange = {
|
||||||
sha: string
|
revisionId: number
|
||||||
pred?: string
|
pred: number | null
|
||||||
succ?: string
|
succ: null
|
||||||
wikiPage: WikiPage
|
wikiPage: Pick<WikiPage, 'id' | 'title'>
|
||||||
user: User
|
user: Pick<User, 'id' | 'name'>
|
||||||
changeType: string
|
kind: 'content' | 'redirect'
|
||||||
|
message: string | null
|
||||||
timestamp: string }
|
timestamp: string }
|
||||||
|
|
||||||
export type WikiPageDiff = {
|
export type WikiPageDiff = {
|
||||||
wikiPageId: number
|
wikiPageId: number
|
||||||
title: string
|
title: string
|
||||||
olderSha: string
|
olderRevisionId: number | null
|
||||||
newerSha: string
|
newerRevisionId: number
|
||||||
diff: WikiPageDiffDiff[] }
|
diff: WikiPageDiffDiff[] }
|
||||||
|
|
||||||
export type WikiPageDiffDiff = {
|
export type WikiPageDiffDiff = {
|
||||||
type: 'context' | 'added' | 'removed'
|
type: 'context' | 'added' | 'removed'
|
||||||
|
|||||||
Reference in New Issue
Block a user