2020-04-22 08:09:29 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
class Wiki
|
|
|
|
extend ::Gitlab::Utils::Override
|
|
|
|
include HasRepository
|
2021-01-19 04:10:32 -05:00
|
|
|
include Repositories::CanHousekeepRepository
|
2020-04-22 08:09:29 -04:00
|
|
|
include Gitlab::Utils::StrongMemoize
|
2020-10-05 11:08:56 -04:00
|
|
|
include GlobalID::Identification
|
2020-04-22 08:09:29 -04:00
|
|
|
|
|
|
|
MARKUPS = { # rubocop:disable Style/MultilineIfModifier
|
|
|
|
'Markdown' => :markdown,
|
|
|
|
'RDoc' => :rdoc,
|
|
|
|
'AsciiDoc' => :asciidoc,
|
|
|
|
'Org' => :org
|
|
|
|
}.freeze unless defined?(MARKUPS)
|
|
|
|
|
|
|
|
CouldNotCreateWikiError = Class.new(StandardError)
|
|
|
|
|
|
|
|
HOMEPAGE = 'home'
|
|
|
|
SIDEBAR = '_sidebar'
|
|
|
|
|
|
|
|
TITLE_ORDER = 'title'
|
|
|
|
CREATED_AT_ORDER = 'created_at'
|
|
|
|
DIRECTION_DESC = 'desc'
|
|
|
|
DIRECTION_ASC = 'asc'
|
|
|
|
|
|
|
|
attr_reader :container, :user
|
|
|
|
|
|
|
|
# Returns a string describing what went wrong after
|
|
|
|
# an operation fails.
|
|
|
|
attr_reader :error_message
|
|
|
|
|
2020-10-05 11:08:56 -04:00
|
|
|
# Support run_after_commit callbacks, since we don't have a DB record
|
|
|
|
# we delegate to the container.
|
|
|
|
delegate :run_after_commit, to: :container
|
|
|
|
|
|
|
|
class << self
|
|
|
|
attr_accessor :container_class
|
|
|
|
|
|
|
|
def for_container(container, user = nil)
|
|
|
|
"#{container.class.name}Wiki".constantize.new(container, user)
|
|
|
|
end
|
|
|
|
|
|
|
|
# This is needed to support repository lookup through Gitlab::GlRepository::Identifier
|
|
|
|
def find_by_id(container_id)
|
|
|
|
container_class.find_by_id(container_id)&.wiki
|
|
|
|
end
|
2020-04-22 08:09:29 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def initialize(container, user = nil)
|
2020-10-05 11:08:56 -04:00
|
|
|
raise ArgumentError, "user must be a User, got #{user.class}" if user && !user.is_a?(User)
|
|
|
|
|
2020-04-22 08:09:29 -04:00
|
|
|
@container = container
|
|
|
|
@user = user
|
2020-10-05 11:08:56 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def ==(other)
|
|
|
|
other.is_a?(self.class) && container == other.container
|
|
|
|
end
|
|
|
|
|
|
|
|
# This is needed in:
|
|
|
|
# - Storage::Hashed
|
|
|
|
# - Gitlab::GlRepository::RepoType#identifier_for_container
|
|
|
|
#
|
|
|
|
# We also need an `#id` to support `build_stubbed` in tests, where the
|
|
|
|
# value doesn't matter.
|
|
|
|
#
|
|
|
|
# NOTE: Wikis don't have a DB record, so this ID can be the same
|
|
|
|
# for two wikis in different containers and should not be expected to
|
|
|
|
# be unique. Use `to_global_id` instead if you need a unique ID.
|
|
|
|
def id
|
|
|
|
container.id
|
2020-04-22 08:09:29 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def path
|
|
|
|
container.path + '.wiki'
|
|
|
|
end
|
|
|
|
|
|
|
|
# Returns the Gitlab::Git::Wiki object.
|
|
|
|
def wiki
|
|
|
|
strong_memoize(:wiki) do
|
2020-05-14 08:08:21 -04:00
|
|
|
create_wiki_repository
|
2020-04-22 08:09:29 -04:00
|
|
|
Gitlab::Git::Wiki.new(repository.raw)
|
|
|
|
end
|
2020-05-14 08:08:21 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def create_wiki_repository
|
|
|
|
repository.create_if_not_exists
|
|
|
|
|
|
|
|
raise CouldNotCreateWikiError unless repository_exists?
|
2020-04-22 08:09:29 -04:00
|
|
|
rescue => err
|
|
|
|
Gitlab::ErrorTracking.track_exception(err, wiki: {
|
|
|
|
container_type: container.class.name,
|
|
|
|
container_id: container.id,
|
|
|
|
full_path: full_path,
|
|
|
|
disk_path: disk_path
|
|
|
|
})
|
|
|
|
|
|
|
|
raise CouldNotCreateWikiError
|
|
|
|
end
|
|
|
|
|
|
|
|
def has_home_page?
|
|
|
|
!!find_page(HOMEPAGE)
|
|
|
|
end
|
|
|
|
|
|
|
|
def empty?
|
2021-02-12 13:08:59 -05:00
|
|
|
!repository_exists? || list_pages(limit: 1).empty?
|
2020-04-22 08:09:29 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def exists?
|
|
|
|
!empty?
|
|
|
|
end
|
|
|
|
|
|
|
|
# Lists wiki pages of the repository.
|
|
|
|
#
|
|
|
|
# limit - max number of pages returned by the method.
|
|
|
|
# sort - criterion by which the pages are sorted.
|
|
|
|
# direction - order of the sorted pages.
|
|
|
|
# load_content - option, which specifies whether the content inside the page
|
|
|
|
# will be loaded.
|
|
|
|
#
|
|
|
|
# Returns an Array of GitLab WikiPage instances or an
|
|
|
|
# empty Array if this Wiki has no pages.
|
|
|
|
def list_pages(limit: 0, sort: nil, direction: DIRECTION_ASC, load_content: false)
|
|
|
|
wiki.list_pages(
|
|
|
|
limit: limit,
|
|
|
|
sort: sort,
|
|
|
|
direction_desc: direction == DIRECTION_DESC,
|
|
|
|
load_content: load_content
|
|
|
|
).map do |page|
|
|
|
|
WikiPage.new(self, page)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-04-28 08:09:44 -04:00
|
|
|
def sidebar_entries(limit: Gitlab::WikiPages::MAX_SIDEBAR_PAGES, **options)
|
|
|
|
pages = list_pages(**options.merge(limit: limit + 1))
|
|
|
|
limited = pages.size > limit
|
|
|
|
pages = pages.first(limit) if limited
|
|
|
|
|
2020-09-24 08:09:37 -04:00
|
|
|
[WikiDirectory.group_pages(pages), limited]
|
2020-04-28 08:09:44 -04:00
|
|
|
end
|
|
|
|
|
2020-09-25 14:09:46 -04:00
|
|
|
# Finds a page within the repository based on a title
|
2020-04-22 08:09:29 -04:00
|
|
|
# or slug.
|
|
|
|
#
|
|
|
|
# title - The human readable or parameterized title of
|
|
|
|
# the page.
|
|
|
|
#
|
|
|
|
# Returns an initialized WikiPage instance or nil
|
|
|
|
def find_page(title, version = nil)
|
|
|
|
page_title, page_dir = page_title_and_dir(title)
|
|
|
|
|
|
|
|
if page = wiki.page(title: page_title, version: version, dir: page_dir)
|
|
|
|
WikiPage.new(self, page)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def find_sidebar(version = nil)
|
|
|
|
find_page(SIDEBAR, version)
|
|
|
|
end
|
|
|
|
|
2021-03-12 10:09:33 -05:00
|
|
|
def find_file(name, version = 'HEAD', load_content: true)
|
2021-04-07 14:09:45 -04:00
|
|
|
data_limit = load_content ? -1 : 0
|
|
|
|
blobs = repository.blobs_at([[version, name]], blob_size_limit: data_limit)
|
2021-03-12 10:09:33 -05:00
|
|
|
|
2021-04-07 14:09:45 -04:00
|
|
|
return if blobs.empty?
|
2021-03-12 10:09:33 -05:00
|
|
|
|
2021-04-07 14:09:45 -04:00
|
|
|
Gitlab::Git::WikiFile.new(blobs.first)
|
2020-04-22 08:09:29 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def create_page(title, content, format = :markdown, message = nil)
|
|
|
|
commit = commit_details(:created, message, title)
|
|
|
|
|
|
|
|
wiki.write_page(title, format.to_sym, content, commit)
|
2020-09-09 17:08:33 -04:00
|
|
|
after_wiki_activity
|
2020-04-22 08:09:29 -04:00
|
|
|
|
2020-09-09 17:08:33 -04:00
|
|
|
true
|
2020-04-22 08:09:29 -04:00
|
|
|
rescue Gitlab::Git::Wiki::DuplicatePageError => e
|
|
|
|
@error_message = "Duplicate page: #{e.message}"
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_page(page, content:, title: nil, format: :markdown, message: nil)
|
|
|
|
commit = commit_details(:updated, message, page.title)
|
|
|
|
|
|
|
|
wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
|
2020-09-09 17:08:33 -04:00
|
|
|
after_wiki_activity
|
2020-04-22 08:09:29 -04:00
|
|
|
|
2020-09-09 17:08:33 -04:00
|
|
|
true
|
2020-04-22 08:09:29 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def delete_page(page, message = nil)
|
|
|
|
return unless page
|
|
|
|
|
2021-03-18 11:09:04 -04:00
|
|
|
if Feature.enabled?(:gitaly_replace_wiki_delete_page, user, default_enabled: :yaml)
|
|
|
|
capture_git_error(:deleted) do
|
|
|
|
repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title))
|
2020-04-22 08:09:29 -04:00
|
|
|
|
2021-03-18 11:09:04 -04:00
|
|
|
after_wiki_activity
|
|
|
|
|
|
|
|
true
|
|
|
|
end
|
|
|
|
else
|
|
|
|
wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
|
|
|
|
after_wiki_activity
|
|
|
|
|
|
|
|
true
|
|
|
|
end
|
2020-04-22 08:09:29 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def page_title_and_dir(title)
|
|
|
|
return unless title
|
|
|
|
|
|
|
|
title_array = title.split("/")
|
|
|
|
title = title_array.pop
|
|
|
|
[title, title_array.join("/")]
|
|
|
|
end
|
|
|
|
|
|
|
|
def ensure_repository
|
|
|
|
raise CouldNotCreateWikiError unless wiki.repository_exists?
|
|
|
|
end
|
|
|
|
|
|
|
|
def hook_attrs
|
|
|
|
{
|
|
|
|
web_url: web_url,
|
|
|
|
git_ssh_url: ssh_url_to_repo,
|
|
|
|
git_http_url: http_url_to_repo,
|
|
|
|
path_with_namespace: full_path,
|
|
|
|
default_branch: default_branch
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
override :repository
|
|
|
|
def repository
|
2020-10-05 11:08:56 -04:00
|
|
|
@repository ||= Gitlab::GlRepository::WIKI.repository_for(self)
|
2020-04-22 08:09:29 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def repository_storage
|
|
|
|
raise NotImplementedError
|
|
|
|
end
|
|
|
|
|
|
|
|
def hashed_storage?
|
|
|
|
raise NotImplementedError
|
|
|
|
end
|
|
|
|
|
|
|
|
override :full_path
|
|
|
|
def full_path
|
|
|
|
container.full_path + '.wiki'
|
|
|
|
end
|
|
|
|
|
|
|
|
# @deprecated use full_path when you need it for an URL route or disk_path when you want to point to the filesystem
|
|
|
|
alias_method :path_with_namespace, :full_path
|
|
|
|
|
|
|
|
override :default_branch
|
|
|
|
def default_branch
|
|
|
|
wiki.class.default_ref
|
|
|
|
end
|
|
|
|
|
|
|
|
def wiki_base_path
|
2020-06-04 14:08:32 -04:00
|
|
|
web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '')
|
2020-04-22 08:09:29 -04:00
|
|
|
end
|
|
|
|
|
2020-09-09 17:08:33 -04:00
|
|
|
# Callbacks for synchronous processing after wiki changes.
|
|
|
|
# These will be executed after any change made through GitLab itself (web UI and API),
|
|
|
|
# but not for Git pushes.
|
|
|
|
def after_wiki_activity
|
|
|
|
end
|
|
|
|
|
|
|
|
# Callbacks for background processing after wiki changes.
|
|
|
|
# These will be executed after any change to the wiki repository.
|
|
|
|
def after_post_receive
|
|
|
|
end
|
|
|
|
|
2021-01-25 13:09:03 -05:00
|
|
|
override :git_garbage_collect_worker_klass
|
|
|
|
def git_garbage_collect_worker_klass
|
|
|
|
Wikis::GitGarbageCollectWorker
|
|
|
|
end
|
|
|
|
|
|
|
|
def cleanup
|
|
|
|
@repository = nil
|
|
|
|
end
|
|
|
|
|
2020-04-22 08:09:29 -04:00
|
|
|
private
|
|
|
|
|
2021-03-18 11:09:04 -04:00
|
|
|
def multi_commit_options(action, message = nil, title = nil)
|
|
|
|
commit_message = build_commit_message(action, message, title)
|
|
|
|
git_user = Gitlab::Git::User.from_gitlab(user)
|
|
|
|
|
|
|
|
{
|
|
|
|
branch_name: repository.root_ref,
|
|
|
|
message: commit_message,
|
|
|
|
author_email: git_user.email,
|
|
|
|
author_name: git_user.name
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2020-04-22 08:09:29 -04:00
|
|
|
def commit_details(action, message = nil, title = nil)
|
2021-03-18 11:09:04 -04:00
|
|
|
commit_message = build_commit_message(action, message, title)
|
2020-04-22 08:09:29 -04:00
|
|
|
git_user = Gitlab::Git::User.from_gitlab(user)
|
|
|
|
|
|
|
|
Gitlab::Git::Wiki::CommitDetails.new(user.id,
|
|
|
|
git_user.username,
|
|
|
|
git_user.name,
|
|
|
|
git_user.email,
|
|
|
|
commit_message)
|
|
|
|
end
|
|
|
|
|
2021-03-18 11:09:04 -04:00
|
|
|
def build_commit_message(action, message, title)
|
|
|
|
message.presence || default_message(action, title)
|
|
|
|
end
|
|
|
|
|
2020-04-22 08:09:29 -04:00
|
|
|
def default_message(action, title)
|
|
|
|
"#{user.username} #{action} page: #{title}"
|
|
|
|
end
|
2021-03-18 11:09:04 -04:00
|
|
|
|
|
|
|
def capture_git_error(action, &block)
|
|
|
|
yield block
|
|
|
|
rescue Gitlab::Git::Index::IndexError,
|
|
|
|
Gitlab::Git::CommitError,
|
|
|
|
Gitlab::Git::PreReceiveError,
|
|
|
|
Gitlab::Git::CommandError,
|
|
|
|
ArgumentError => error
|
|
|
|
|
|
|
|
Gitlab::ErrorTracking.log_exception(error, action: action, wiki_id: id)
|
|
|
|
|
|
|
|
false
|
|
|
|
end
|
2020-04-22 08:09:29 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
Wiki.prepend_if_ee('EE::Wiki')
|